Browse Source

Feature/filters v2 (#835)

Improved filters with new filter model and reduced dependency to EDM (OData). Also generates the query model in the backend instead of the frontend.
pull/837/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
3f3d355b0c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 792
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs
  2. 363
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx
  3. 65
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs
  4. 13
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  5. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs
  7. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs
  9. 240
      backend/src/Squidex.Domain.Apps.Core.Operations/FieldDescriptions.cs
  10. 69
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs
  11. 142
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs
  12. 101
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/AssetQueryModel.cs
  13. 76
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/ContentQueryModel.cs
  14. 86
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterExtensions.cs
  15. 206
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterVisitor.cs
  16. 46
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/SharedSchemas.cs
  17. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentJsonSchema.cs
  18. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeBuilder.cs
  19. 19
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs
  20. 27
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs
  21. 306
      backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs
  22. 201
      backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx
  23. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeJintExtension.cs
  24. 26
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs
  25. 42
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs
  26. 13
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringWordsJintExtension.cs
  27. 16
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptDescriptor.cs
  28. 21
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  29. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JsonType.cs
  30. 19
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptScope.cs
  31. 307
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs
  32. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingValue.cs
  33. 26
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  34. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs
  35. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  36. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs
  37. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/AdaptIdVisitor.cs
  38. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs
  39. 15
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs
  40. 91
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs
  41. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs
  42. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs
  43. 112
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  44. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs
  45. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs
  46. 126
      backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs
  47. 141
      backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx
  48. 313
      backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptingCompletion.cs
  49. 13
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  50. 4
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs
  51. 5
      backend/src/Squidex.Infrastructure/CollectionExtensions.cs
  52. 162
      backend/src/Squidex.Infrastructure/Properties/Resources.Designer.cs
  53. 153
      backend/src/Squidex.Infrastructure/Properties/Resources.resx
  54. 3
      backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs
  55. 99
      backend/src/Squidex.Infrastructure/Queries/CompareOperatorTypeConverter.cs
  56. 14
      backend/src/Squidex.Infrastructure/Queries/FilterField.cs
  57. 104
      backend/src/Squidex.Infrastructure/Queries/FilterSchema.cs
  58. 24
      backend/src/Squidex.Infrastructure/Queries/FilterSchemaType.cs
  59. 46
      backend/src/Squidex.Infrastructure/Queries/Json/CompareOperatorJsonConverter.cs
  60. 70
      backend/src/Squidex.Infrastructure/Queries/Json/Errors.cs
  61. 47
      backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs
  62. 44
      backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs
  63. 86
      backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs
  64. 47
      backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs
  65. 56
      backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs
  66. 122
      backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs
  67. 125
      backend/src/Squidex.Infrastructure/Queries/OData/EdmModelConverter.cs
  68. 136
      backend/src/Squidex.Infrastructure/Queries/QueryModel.cs
  69. 14
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  70. 19
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  71. 4
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs
  72. 44
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  73. 3
      backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/ConnectController.cs
  74. 13
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  75. 6
      backend/src/Squidex/Config/Domain/RuleServices.cs
  76. 2
      backend/src/Squidex/Config/Domain/SerializationServices.cs
  77. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs
  78. 49
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs
  79. 104
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs
  80. 47
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs
  81. 67
      backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs
  82. 156
      backend/tests/Squidex.Infrastructure.Tests/Queries/FilterSchemaTests.cs
  83. 280
      backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs
  84. 141
      backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs
  85. 8
      backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs
  86. 8
      backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestUtils.cs
  87. 6
      frontend/src/app/features/assets/pages/assets-page.component.html
  88. 5
      frontend/src/app/features/content/pages/contents/contents-page.component.html
  89. 16
      frontend/src/app/features/content/pages/contents/contents-page.component.ts
  90. 4
      frontend/src/app/framework/angular/forms/editors/autocomplete.component.html
  91. 5
      frontend/src/app/framework/angular/forms/editors/autocomplete.component.ts
  92. 8
      frontend/src/app/framework/angular/forms/editors/dropdown.component.html
  93. 13
      frontend/src/app/framework/angular/forms/editors/dropdown.component.ts
  94. 2
      frontend/src/app/framework/angular/forms/editors/tag-editor.component.html
  95. 2
      frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts
  96. 2
      frontend/src/app/shared/components/assets/assets-selector.component.html
  97. 3
      frontend/src/app/shared/components/references/content-selector.component.html
  98. 22
      frontend/src/app/shared/components/references/content-selector.component.ts
  99. 63
      frontend/src/app/shared/components/search/queries/filter-comparison.component.html
  100. 46
      frontend/src/app/shared/components/search/queries/filter-comparison.component.ts

792
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs

@ -0,0 +1,792 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Squidex.Domain.Apps.Core {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class FieldDescriptions {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal FieldDescriptions() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Core.FieldDescriptions", typeof(FieldDescriptions).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to The ID of the current app..
/// </summary>
public static string AppId {
get {
return ResourceManager.GetString("AppId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The name of the current app..
/// </summary>
public static string AppName {
get {
return ResourceManager.GetString("AppName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The asset..
/// </summary>
public static string Asset {
get {
return ResourceManager.GetString("Asset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The hash of the file. Can be null for old files..
/// </summary>
public static string AssetFileHash {
get {
return ResourceManager.GetString("AssetFileHash", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The file name of the asset..
/// </summary>
public static string AssetFileName {
get {
return ResourceManager.GetString("AssetFileName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The size of the file in bytes..
/// </summary>
public static string AssetFileSize {
get {
return ResourceManager.GetString("AssetFileSize", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The file type (file extension) of the asset..
/// </summary>
public static string AssetFileType {
get {
return ResourceManager.GetString("AssetFileType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The version of the file..
/// </summary>
public static string AssetFileVersion {
get {
return ResourceManager.GetString("AssetFileVersion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Determines if the uploaded file is an image..
/// </summary>
public static string AssetIsImage {
get {
return ResourceManager.GetString("AssetIsImage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to True, when the asset is not public..
/// </summary>
public static string AssetIsProtected {
get {
return ResourceManager.GetString("AssetIsProtected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The asset metadata..
/// </summary>
public static string AssetMetadata {
get {
return ResourceManager.GetString("AssetMetadata", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The type of the image..
/// </summary>
public static string AssetMetadataText {
get {
return ResourceManager.GetString("AssetMetadataText", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The asset metadata with name &apos;name&apos;..
/// </summary>
public static string AssetMetadataValue {
get {
return ResourceManager.GetString("AssetMetadataValue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The mime type..
/// </summary>
public static string AssetMimeType {
get {
return ResourceManager.GetString("AssetMimeType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The id of the parent folder. Empty for files without parent..
/// </summary>
public static string AssetParentId {
get {
return ResourceManager.GetString("AssetParentId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The full path in the folder hierarchy as array of folder infos..
/// </summary>
public static string AssetParentPath {
get {
return ResourceManager.GetString("AssetParentPath", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The height of the image in pixels if the asset is an image..
/// </summary>
public static string AssetPixelHeight {
get {
return ResourceManager.GetString("AssetPixelHeight", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The width of the image in pixels if the asset is an image..
/// </summary>
public static string AssetPixelWidth {
get {
return ResourceManager.GetString("AssetPixelWidth", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The assets..
/// </summary>
public static string AssetsItems {
get {
return ResourceManager.GetString("AssetsItems", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The file name as slug..
/// </summary>
public static string AssetSlug {
get {
return ResourceManager.GetString("AssetSlug", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The source URL of the asset..
/// </summary>
public static string AssetSourceUrl {
get {
return ResourceManager.GetString("AssetSourceUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The total count of assets..
/// </summary>
public static string AssetsTotal {
get {
return ResourceManager.GetString("AssetsTotal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The asset tags..
/// </summary>
public static string AssetTags {
get {
return ResourceManager.GetString("AssetTags", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The thumbnail URL to the asset..
/// </summary>
public static string AssetThumbnailUrl {
get {
return ResourceManager.GetString("AssetThumbnailUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The type of the asset..
/// </summary>
public static string AssetType {
get {
return ResourceManager.GetString("AssetType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The URL to the asset..
/// </summary>
public static string AssetUrl {
get {
return ResourceManager.GetString("AssetUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The executed command..
/// </summary>
public static string Command {
get {
return ResourceManager.GetString("Command", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} field ({1} component)..
/// </summary>
public static string ComponentField {
get {
return ResourceManager.GetString("ComponentField", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The schema id to identity the component type..
/// </summary>
public static string ComponentSchemaId {
get {
return ResourceManager.GetString("ComponentSchemaId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} nested field..
/// </summary>
public static string ContentArrayField {
get {
return ResourceManager.GetString("ContentArrayField", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The data of the content..
/// </summary>
public static string ContentData {
get {
return ResourceManager.GetString("ContentData", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The previous data of the content..
/// </summary>
public static string ContentDataOld {
get {
return ResourceManager.GetString("ContentDataOld", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} field..
/// </summary>
public static string ContentField {
get {
return ResourceManager.GetString("ContentField", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The flat data of the content..
/// </summary>
public static string ContentFlatData {
get {
return ResourceManager.GetString("ContentFlatData", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The new status of the content..
/// </summary>
public static string ContentNewStatus {
get {
return ResourceManager.GetString("ContentNewStatus", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The new status color of the content..
/// </summary>
public static string ContentNewStatusColor {
get {
return ResourceManager.GetString("ContentNewStatusColor", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} field ({1})..
/// </summary>
public static string ContentPartitionField {
get {
return ResourceManager.GetString("ContentPartitionField", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The data for the content..
/// </summary>
public static string ContentRequestData {
get {
return ResourceManager.GetString("ContentRequestData", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The timestamp when the status should be changed..
/// </summary>
public static string ContentRequestDueTime {
get {
return ResourceManager.GetString("ContentRequestDueTime", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The optional custom content ID..
/// </summary>
public static string ContentRequestOptionalId {
get {
return ResourceManager.GetString("ContentRequestOptionalId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The initial status..
/// </summary>
public static string ContentRequestOptionalStatus {
get {
return ResourceManager.GetString("ContentRequestOptionalStatus", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Set to true to autopublish content on create..
/// </summary>
public static string ContentRequestPublish {
get {
return ResourceManager.GetString("ContentRequestPublish", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The status for the content..
/// </summary>
public static string ContentRequestStatus {
get {
return ResourceManager.GetString("ContentRequestStatus", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The name of the schema..
/// </summary>
public static string ContentSchema {
get {
return ResourceManager.GetString("ContentSchema", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The ID of the schema..
/// </summary>
public static string ContentSchemaId {
get {
return ResourceManager.GetString("ContentSchemaId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The display name of the schema..
/// </summary>
public static string ContentSchemaName {
get {
return ResourceManager.GetString("ContentSchemaName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The contents..
/// </summary>
public static string ContentsItems {
get {
return ResourceManager.GetString("ContentsItems", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The status of the content..
/// </summary>
public static string ContentStatus {
get {
return ResourceManager.GetString("ContentStatus", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The status color of the content..
/// </summary>
public static string ContentStatusColor {
get {
return ResourceManager.GetString("ContentStatusColor", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The previous status of the content..
/// </summary>
public static string ContentStatusOld {
get {
return ResourceManager.GetString("ContentStatusOld", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The total count of contents..
/// </summary>
public static string ContentsTotal {
get {
return ResourceManager.GetString("ContentsTotal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The URL to the content..
/// </summary>
public static string ContentUrl {
get {
return ResourceManager.GetString("ContentUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The context object holding all values..
/// </summary>
public static string Context {
get {
return ResourceManager.GetString("Context", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The edit token..
/// </summary>
public static string EditToken {
get {
return ResourceManager.GetString("EditToken", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The timestamp when the object was created..
/// </summary>
public static string EntityCreated {
get {
return ResourceManager.GetString("EntityCreated", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The user who created the object..
/// </summary>
public static string EntityCreatedBy {
get {
return ResourceManager.GetString("EntityCreatedBy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The expected version..
/// </summary>
public static string EntityExpectedVersion {
get {
return ResourceManager.GetString("EntityExpectedVersion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The ID of the object (usually GUID)..
/// </summary>
public static string EntityId {
get {
return ResourceManager.GetString("EntityId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to True when deleted..
/// </summary>
public static string EntityIsDeleted {
get {
return ResourceManager.GetString("EntityIsDeleted", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The timestamp when the object was updated the last time..
/// </summary>
public static string EntityLastModified {
get {
return ResourceManager.GetString("EntityLastModified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The user who updated the object the last time..
/// </summary>
public static string EntityLastModifiedBy {
get {
return ResourceManager.GetString("EntityLastModifiedBy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to True when the entity should be deleted permanently..
/// </summary>
public static string EntityRequestDeletePermanent {
get {
return ResourceManager.GetString("EntityRequestDeletePermanent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The version of the objec..
/// </summary>
public static string EntityVersion {
get {
return ResourceManager.GetString("EntityVersion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The path to the json value..
/// </summary>
public static string JsonPath {
get {
return ResourceManager.GetString("JsonPath", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The current operation..
/// </summary>
public static string Operation {
get {
return ResourceManager.GetString("Operation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Optional OData filter..
/// </summary>
public static string QueryFilter {
get {
return ResourceManager.GetString("QueryFilter", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Comma separated list of object IDs. Overrides all other query parameters..
/// </summary>
public static string QueryIds {
get {
return ResourceManager.GetString("QueryIds", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Optional OData order definition..
/// </summary>
public static string QueryOrderBy {
get {
return ResourceManager.GetString("QueryOrderBy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to JSON query as well formatted json string. Overrides all other query parameters, except &apos;ids&apos;..
/// </summary>
public static string QueryQ {
get {
return ResourceManager.GetString("QueryQ", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Optional OData full text search..
/// </summary>
public static string QuerySearch {
get {
return ResourceManager.GetString("QuerySearch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Optional number of contents to skip..
/// </summary>
public static string QuerySkip {
get {
return ResourceManager.GetString("QuerySkip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Optional number of contents to take..
/// </summary>
public static string QueryTop {
get {
return ResourceManager.GetString("QueryTop", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The optional version of the content to retrieve an older instance (not cached)..
/// </summary>
public static string QueryVersion {
get {
return ResourceManager.GetString("QueryVersion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Information about the current user..
/// </summary>
public static string User {
get {
return ResourceManager.GetString("User", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The additional properties of the user..
/// </summary>
public static string UserClaims {
get {
return ResourceManager.GetString("UserClaims", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The display name of the user..
/// </summary>
public static string UserDisplayName {
get {
return ResourceManager.GetString("UserDisplayName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The email address of the current user..
/// </summary>
public static string UserEmail {
get {
return ResourceManager.GetString("UserEmail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The ID of the user..
/// </summary>
public static string UserId {
get {
return ResourceManager.GetString("UserId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to True when the current user is a client, which is typically the case when the request is made from the API..
/// </summary>
public static string UserIsClient {
get {
return ResourceManager.GetString("UserIsClient", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to True when the current user is a user, which is typically the case when the request is made in the UI..
/// </summary>
public static string UserIsUser {
get {
return ResourceManager.GetString("UserIsUser", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The list of additional properties that have the name &apos;name&apos;..
/// </summary>
public static string UsersClaimsValue {
get {
return ResourceManager.GetString("UsersClaimsValue", resourceCulture);
}
}
}
}

363
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx

@ -0,0 +1,363 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AppId" xml:space="preserve">
<value>The ID of the current app.</value>
</data>
<data name="AppName" xml:space="preserve">
<value>The name of the current app.</value>
</data>
<data name="Asset" xml:space="preserve">
<value>The asset.</value>
</data>
<data name="AssetFileHash" xml:space="preserve">
<value>The hash of the file. Can be null for old files.</value>
</data>
<data name="AssetFileName" xml:space="preserve">
<value>The file name of the asset.</value>
</data>
<data name="AssetFileSize" xml:space="preserve">
<value>The size of the file in bytes.</value>
</data>
<data name="AssetFileType" xml:space="preserve">
<value>The file type (file extension) of the asset.</value>
</data>
<data name="AssetFileVersion" xml:space="preserve">
<value>The version of the file.</value>
</data>
<data name="AssetIsImage" xml:space="preserve">
<value>Determines if the uploaded file is an image.</value>
</data>
<data name="AssetIsProtected" xml:space="preserve">
<value>True, when the asset is not public.</value>
</data>
<data name="AssetMetadata" xml:space="preserve">
<value>The asset metadata.</value>
</data>
<data name="AssetMetadataText" xml:space="preserve">
<value>The type of the image.</value>
</data>
<data name="AssetMetadataValue" xml:space="preserve">
<value>The asset metadata with name 'name'.</value>
</data>
<data name="AssetMimeType" xml:space="preserve">
<value>The mime type.</value>
</data>
<data name="AssetParentId" xml:space="preserve">
<value>The id of the parent folder. Empty for files without parent.</value>
</data>
<data name="AssetParentPath" xml:space="preserve">
<value>The full path in the folder hierarchy as array of folder infos.</value>
</data>
<data name="AssetPixelHeight" xml:space="preserve">
<value>The height of the image in pixels if the asset is an image.</value>
</data>
<data name="AssetPixelWidth" xml:space="preserve">
<value>The width of the image in pixels if the asset is an image.</value>
</data>
<data name="AssetsItems" xml:space="preserve">
<value>The assets.</value>
</data>
<data name="AssetSlug" xml:space="preserve">
<value>The file name as slug.</value>
</data>
<data name="AssetSourceUrl" xml:space="preserve">
<value>The source URL of the asset.</value>
</data>
<data name="AssetsTotal" xml:space="preserve">
<value>The total count of assets.</value>
</data>
<data name="AssetTags" xml:space="preserve">
<value>The asset tags.</value>
</data>
<data name="AssetThumbnailUrl" xml:space="preserve">
<value>The thumbnail URL to the asset.</value>
</data>
<data name="AssetType" xml:space="preserve">
<value>The type of the asset.</value>
</data>
<data name="AssetUrl" xml:space="preserve">
<value>The URL to the asset.</value>
</data>
<data name="Command" xml:space="preserve">
<value>The executed command.</value>
</data>
<data name="ComponentField" xml:space="preserve">
<value>{0} field ({1} component).</value>
</data>
<data name="ComponentSchemaId" xml:space="preserve">
<value>The schema id to identity the component type.</value>
</data>
<data name="ContentArrayField" xml:space="preserve">
<value>{0} nested field.</value>
</data>
<data name="ContentData" xml:space="preserve">
<value>The data of the content.</value>
</data>
<data name="ContentDataOld" xml:space="preserve">
<value>The previous data of the content.</value>
</data>
<data name="ContentField" xml:space="preserve">
<value>{0} field.</value>
</data>
<data name="ContentFlatData" xml:space="preserve">
<value>The flat data of the content.</value>
</data>
<data name="ContentNewStatus" xml:space="preserve">
<value>The new status of the content.</value>
</data>
<data name="ContentNewStatusColor" xml:space="preserve">
<value>The new status color of the content.</value>
</data>
<data name="ContentPartitionField" xml:space="preserve">
<value>{0} field ({1}).</value>
</data>
<data name="ContentRequestData" xml:space="preserve">
<value>The data for the content.</value>
</data>
<data name="ContentRequestDueTime" xml:space="preserve">
<value>The timestamp when the status should be changed.</value>
</data>
<data name="ContentRequestOptionalId" xml:space="preserve">
<value>The optional custom content ID.</value>
</data>
<data name="ContentRequestOptionalStatus" xml:space="preserve">
<value>The initial status.</value>
</data>
<data name="ContentRequestPublish" xml:space="preserve">
<value>Set to true to autopublish content on create.</value>
</data>
<data name="ContentRequestStatus" xml:space="preserve">
<value>The status for the content.</value>
</data>
<data name="ContentSchema" xml:space="preserve">
<value>The name of the schema.</value>
</data>
<data name="ContentSchemaId" xml:space="preserve">
<value>The ID of the schema.</value>
</data>
<data name="ContentSchemaName" xml:space="preserve">
<value>The display name of the schema.</value>
</data>
<data name="ContentsItems" xml:space="preserve">
<value>The contents.</value>
</data>
<data name="ContentStatus" xml:space="preserve">
<value>The status of the content.</value>
</data>
<data name="ContentStatusColor" xml:space="preserve">
<value>The status color of the content.</value>
</data>
<data name="ContentStatusOld" xml:space="preserve">
<value>The previous status of the content.</value>
</data>
<data name="ContentsTotal" xml:space="preserve">
<value>The total count of contents.</value>
</data>
<data name="ContentUrl" xml:space="preserve">
<value>The URL to the content.</value>
</data>
<data name="Context" xml:space="preserve">
<value>The context object holding all values.</value>
</data>
<data name="EditToken" xml:space="preserve">
<value>The edit token.</value>
</data>
<data name="EntityCreated" xml:space="preserve">
<value>The timestamp when the object was created.</value>
</data>
<data name="EntityCreatedBy" xml:space="preserve">
<value>The user who created the object.</value>
</data>
<data name="EntityExpectedVersion" xml:space="preserve">
<value>The expected version.</value>
</data>
<data name="EntityId" xml:space="preserve">
<value>The ID of the object (usually GUID).</value>
</data>
<data name="EntityIsDeleted" xml:space="preserve">
<value>True when deleted.</value>
</data>
<data name="EntityLastModified" xml:space="preserve">
<value>The timestamp when the object was updated the last time.</value>
</data>
<data name="EntityLastModifiedBy" xml:space="preserve">
<value>The user who updated the object the last time.</value>
</data>
<data name="EntityRequestDeletePermanent" xml:space="preserve">
<value>True when the entity should be deleted permanently.</value>
</data>
<data name="EntityVersion" xml:space="preserve">
<value>The version of the objec.</value>
</data>
<data name="JsonPath" xml:space="preserve">
<value>The path to the json value.</value>
</data>
<data name="Operation" xml:space="preserve">
<value>The current operation.</value>
</data>
<data name="QueryFilter" xml:space="preserve">
<value>Optional OData filter.</value>
</data>
<data name="QueryIds" xml:space="preserve">
<value>Comma separated list of object IDs. Overrides all other query parameters.</value>
</data>
<data name="QueryOrderBy" xml:space="preserve">
<value>Optional OData order definition.</value>
</data>
<data name="QueryQ" xml:space="preserve">
<value>JSON query as well formatted json string. Overrides all other query parameters, except 'ids'.</value>
</data>
<data name="QuerySearch" xml:space="preserve">
<value>Optional OData full text search.</value>
</data>
<data name="QuerySkip" xml:space="preserve">
<value>Optional number of contents to skip.</value>
</data>
<data name="QueryTop" xml:space="preserve">
<value>Optional number of contents to take.</value>
</data>
<data name="QueryVersion" xml:space="preserve">
<value>The optional version of the content to retrieve an older instance (not cached).</value>
</data>
<data name="User" xml:space="preserve">
<value>Information about the current user.</value>
</data>
<data name="UserClaims" xml:space="preserve">
<value>The additional properties of the user.</value>
</data>
<data name="UserDisplayName" xml:space="preserve">
<value>The display name of the user.</value>
</data>
<data name="UserEmail" xml:space="preserve">
<value>The email address of the current user.</value>
</data>
<data name="UserId" xml:space="preserve">
<value>The ID of the user.</value>
</data>
<data name="UserIsClient" xml:space="preserve">
<value>True when the current user is a client, which is typically the case when the request is made from the API.</value>
</data>
<data name="UserIsUser" xml:space="preserve">
<value>True when the current user is a user, which is typically the case when the request is made in the UI.</value>
</data>
<data name="UsersClaimsValue" xml:space="preserve">
<value>The list of additional properties that have the name 'name'.</value>
</data>
</root>

65
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using NamedIdStatic = Squidex.Infrastructure.NamedId; using NamedIdStatic = Squidex.Infrastructure.NamedId;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
@ -23,18 +22,6 @@ namespace Squidex.Domain.Apps.Core.Schemas
return fields.Where(x => IsForApi(x, withHidden)); return fields.Where(x => IsForApi(x, withHidden));
} }
public static IEnumerable<IRootField> GetSharedFields(this ResolvedComponents components, ReadonlyList<DomainId>? schemaIds, bool withHidden)
{
if (schemaIds == null || schemaIds.Count == 0)
{
return Enumerable.Empty<IRootField>();
}
var allFields = components.Resolve(schemaIds).Values.SelectMany(x => x.Fields.ForApi(withHidden));
return allFields.GroupBy(x => new { x.Name, Type = x.RawProperties }).SingleGroups();
}
public static bool IsForApi<T>(this T field, bool withHidden = false) where T : IField public static bool IsForApi<T>(this T field, bool withHidden = false) where T : IField
{ {
return (withHidden || !field.IsHidden) && !field.RawProperties.IsUIProperty(); return (withHidden || !field.IsHidden) && !field.RawProperties.IsUIProperty();
@ -59,14 +46,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
if (parentId != null) if (parentId != null)
{ {
return schema.UpdateField(parentId.Value, f => return schema.UpdateField(parentId.Value, field =>
{ {
if (f is ArrayField arrayField) if (field is ArrayField arrayField)
{ {
return arrayField.ReorderFields(ids); return arrayField.ReorderFields(ids);
} }
return f; return field;
}); });
} }
@ -77,14 +64,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
if (parentId != null) if (parentId != null)
{ {
return schema.UpdateField(parentId.Value, f => return schema.UpdateField(parentId.Value, field =>
{ {
if (f is ArrayField arrayField) if (field is ArrayField arrayField)
{ {
return arrayField.DeleteField(fieldId); return arrayField.DeleteField(fieldId);
} }
return f; return field;
}); });
} }
@ -95,14 +82,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
if (parentId != null) if (parentId != null)
{ {
return schema.UpdateField(parentId.Value, f => return schema.UpdateField(parentId.Value, field =>
{ {
if (f is ArrayField arrayField) if (field is ArrayField arrayField)
{ {
return arrayField.UpdateField(fieldId, n => n.Lock()); return arrayField.UpdateField(fieldId, f => f.Lock());
} }
return f; return field;
}); });
} }
@ -131,14 +118,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
if (parentId != null) if (parentId != null)
{ {
return schema.UpdateField(parentId.Value, f => return schema.UpdateField(parentId.Value, field =>
{ {
if (f is ArrayField arrayField) if (field is ArrayField arrayField)
{ {
return arrayField.UpdateField(fieldId, n => n.Show()); return arrayField.UpdateField(fieldId, f => f.Show());
} }
return f; return field;
}); });
} }
@ -149,14 +136,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
if (parentId != null) if (parentId != null)
{ {
return schema.UpdateField(parentId.Value, f => return schema.UpdateField(parentId.Value, field =>
{ {
if (f is ArrayField arrayField) if (field is ArrayField arrayField)
{ {
return arrayField.UpdateField(fieldId, n => n.Enable()); return arrayField.UpdateField(fieldId, f => f.Enable());
} }
return f; return field;
}); });
} }
@ -167,14 +154,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
if (parentId != null) if (parentId != null)
{ {
return schema.UpdateField(parentId.Value, f => return schema.UpdateField(parentId.Value, field =>
{ {
if (f is ArrayField arrayField) if (field is ArrayField arrayField)
{ {
return arrayField.UpdateField(fieldId, n => n.Disable()); return arrayField.UpdateField(fieldId, f => f.Disable());
} }
return f; return field;
}); });
} }
@ -185,14 +172,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
if (parentId != null) if (parentId != null)
{ {
return schema.UpdateField(parentId.Value, f => return schema.UpdateField(parentId.Value, field =>
{ {
if (f is ArrayField arrayField) if (field is ArrayField arrayField)
{ {
return arrayField.UpdateField(fieldId, n => n.Update(properties)); return arrayField.UpdateField(fieldId, f => f.Update(properties));
} }
return f; return field;
}); });
} }

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

@ -28,4 +28,17 @@
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> <AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Update="FieldDescriptions.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>FieldDescriptions.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="FieldDescriptions.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>FieldDescriptions.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project> </Project>

2
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
private static readonly StringFormatter Instance = new StringFormatter(); private static readonly StringFormatter Instance = new StringFormatter();
public sealed record Args(IJsonValue Value); public record struct Args(IJsonValue Value);
private StringFormatter() private StringFormatter()
{ {

2
backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.DefaultValues
{ {
private static readonly DefaultValueFactory Instance = new DefaultValueFactory(); private static readonly DefaultValueFactory Instance = new DefaultValueFactory();
public sealed record Args(Instant Now, string Partition); public record struct Args(Instant Now, string Partition);
private DefaultValueFactory() private DefaultValueFactory()
{ {

2
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{ {
private static readonly ReferencesCleaner Instance = new ReferencesCleaner(); private static readonly ReferencesCleaner Instance = new ReferencesCleaner();
public sealed record Args(IJsonValue Value, ISet<DomainId> ValidIds); public record struct Args(IJsonValue Value, ISet<DomainId> ValidIds);
private ReferencesCleaner() private ReferencesCleaner()
{ {

2
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{ {
private static readonly ReferencesExtractor Instance = new ReferencesExtractor(); private static readonly ReferencesExtractor Instance = new ReferencesExtractor();
public sealed record Args(IJsonValue Value, ISet<DomainId> Result, int Take, ResolvedComponents Components); public record struct Args(IJsonValue Value, ISet<DomainId> Result, int Take, ResolvedComponents Components);
private ReferencesExtractor() private ReferencesExtractor()
{ {

240
backend/src/Squidex.Domain.Apps.Core.Operations/FieldDescriptions.cs

@ -1,240 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core
{
public static class FieldDescriptions
{
public static string AppId =>
"The ID of the current app.";
public static string AppName =>
"The name of the current app.";
public static string Asset =>
"The asset.";
public static string AssetFileHash =>
"The hash of the file. Can be null for old files.";
public static string AssetFileName =>
"The file name of the asset.";
public static string AssetFileSize =>
"The size of the file in bytes.";
public static string AssetFileType =>
"The file type (file extension) of the asset.";
public static string AssetFileVersion =>
"The version of the file.";
public static string AssetIsImage =>
"Determines if the uploaded file is an image.";
public static string AssetIsProtected =>
"True, when the asset is not public.";
public static string AssetMetadata =>
"The asset metadata.";
public static string AssetMetadataText =>
"The type of the image.";
public static string AssetMetadataValue =>
"The asset metadata with name 'name'.";
public static string AssetMimeType =>
"The mime type.";
public static string AssetParentId =>
"The id of the parent folder. Empty for files without parent.";
public static string AssetParentPath =>
"The full path in the folder hierarchy as array of folder infos.";
public static string AssetPixelHeight =>
"The height of the image in pixels if the asset is an image.";
public static string AssetPixelWidth =>
"The width of the image in pixels if the asset is an image.";
public static string AssetsItems =>
"The assets.";
public static string AssetSlug =>
"The file name as slug.";
public static string AssetSourceUrl =>
"The source URL of the asset.";
public static string AssetsTotal =>
"The total count of assets.";
public static string AssetTags =>
"The asset tags.";
public static string AssetThumbnailUrl =>
"The thumbnail URL to the asset.";
public static string AssetType =>
"The type of the image.";
public static string AssetUrl =>
"The URL to the asset.";
public static string Command =>
"The executed command.";
public static string ContentData =>
"The data of the content.";
public static string ContentDataOld =>
"The previous data of the content.";
public static string ContentFlatData =>
"The flat data of the content.";
public static string ContentNewStatus =>
"The new status of the content.";
public static string ContentNewStatusColor =>
"The new status color of the content.";
public static string ContentRequestData =>
"The data for the content.";
public static string ContentRequestDueTime =>
"The timestamp when the status should be changed.";
public static string ContentRequestOptionalId =>
"The optional custom content ID.";
public static string ContentRequestOptionalStatus =>
"The initial status.";
public static string ContentRequestPublish =>
"Set to true to autopublish content on create.";
public static string ContentRequestStatus =>
"The status for the content.";
public static string ContentSchema =>
"The name of the schema.";
public static string ContentSchemaId =>
"The ID of the schema.";
public static string ContentSchemaName =>
"The display name of the schema.";
public static string ContentsItems =>
$"The contents.";
public static string ContentStatus =>
"The status of the content.";
public static string ContentStatusColor =>
"The status color of the content.";
public static string ContentStatusOld =>
"The previous status of the content.";
public static string ContentsTotal =>
$"The total count of contents.";
public static string ContentUrl =>
"The URL to the content.";
public static string Context =>
"The context object holding all values.";
public static string EditToken =>
"The edit token.";
public static string EntityCreated =>
"The timestamp when the object was created.";
public static string EntityCreatedBy =>
"The user who created the object.";
public static string EntityExpectedVersion =>
"The expected version.";
public static string EntityId =>
"The ID of the object.";
public static string EntityIsDeleted =>
"True when deleted.";
public static string EntityLastModified =>
"The timestamp when the object was updated the last time.";
public static string EntityLastModifiedBy =>
"The user who updated the object the last time.";
public static string EntityRequestDeletePermanent =>
"True when the entity should be deleted permanently.";
public static string EntityVersion =>
"The version of the object (usually GUID).";
public static string JsonPath =>
"The path to the json value.";
public static string Operation =>
"The current operation.";
public static string QueryFilter =>
"Optional OData filter.";
public static string QueryIds =>
"Comma separated list of object IDs. Overrides all other query parameters.";
public static string QueryOrderBy =>
"Optional OData order definition.";
public static string QueryQ =>
"JSON query as well formatted json string. Overrides all other query parameters, except 'ids'.";
public static string QuerySearch =>
"Optional OData full text search.";
public static string QuerySkip =>
"Optional number of contents to skip.";
public static string QueryTop =>
"Optional number of contents to take.";
public static string QueryVersion =>
"The optional version of the content to retrieve an older instance (not cached).";
public static string User =>
"Information about the current user.";
public static string UserClaims =>
"The additional properties of the user.";
public static string UserDisplayName =>
"The display name of the user.";
public static string UserEmail =>
"The email address of the current user.";
public static string UserId =>
"The ID of the user.";
public static string UserIsClient =>
"True when the current user is a client, which is typically the case when the request is made from the API.";
public static string UserIsUser =>
"True when the current user is a user, which is typically the case when the request is made in the UI.";
public static string UsersClaimsValue =>
"The list of additional properties that have the name 'name'.";
}
}

69
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs

@ -1,69 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData.Edm;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Text;
namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
{
public delegate (EdmComplexType Type, bool Created) EdmTypeFactory(string name);
public static class EdmSchemaExtensions
{
public static EdmComplexType BuildEdmType(this Schema schema, bool withHidden, PartitionResolver partitionResolver, EdmTypeFactory factory,
ResolvedComponents components)
{
Guard.NotNull(factory, nameof(factory));
Guard.NotNull(partitionResolver, nameof(partitionResolver));
var (edmType, _) = factory("Data");
foreach (var field in schema.FieldsByName.Values)
{
if (!field.IsForApi(withHidden))
{
continue;
}
var fieldEdmType = EdmTypeVisitor.BuildType(field, factory, components);
if (fieldEdmType == null)
{
continue;
}
var (partitionType, created) = factory($"Data.{field.Name.ToPascalCase()}");
if (created)
{
var partitioning = partitionResolver(field.Partitioning);
foreach (var partitionKey in partitioning.AllKeys)
{
partitionType.AddStructuralProperty(partitionKey.EscapeEdmField(), fieldEdmType);
}
}
edmType.AddStructuralProperty(field.Name.EscapeEdmField(), new EdmComplexTypeReference(partitionType, false));
}
return edmType;
}
public static string EscapeEdmField(this string field)
{
return field.Replace("-", "_", StringComparison.Ordinal);
}
public static string UnescapeEdmField(this string field)
{
return field.Replace("_", "-", StringComparison.Ordinal);
}
}
}

142
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs

@ -1,142 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData.Edm;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Text;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
{
internal sealed class EdmTypeVisitor : IFieldVisitor<IEdmTypeReference?, EdmTypeVisitor.Args>
{
private const int MaxDepth = 5;
private static readonly EdmComplexType JsonType = new EdmComplexType("Squidex", "Json", null, false, true);
private static readonly EdmTypeVisitor Instance = new EdmTypeVisitor();
public sealed record Args(EdmTypeFactory Factory, ResolvedComponents Components, int Level = 0);
private EdmTypeVisitor()
{
}
public static IEdmTypeReference? BuildType(IField field, EdmTypeFactory factory, ResolvedComponents components)
{
var args = new Args(factory, components);
return field.Accept(Instance, args);
}
public IEdmTypeReference? Visit(IArrayField field, Args args)
{
return CreateNestedType(field, field.Fields.ForApi(true), args);
}
public IEdmTypeReference? Visit(IField<AssetsFieldProperties> field, Args args)
{
return CreatePrimitive(EdmPrimitiveTypeKind.String, field);
}
public IEdmTypeReference? Visit(IField<BooleanFieldProperties> field, Args args)
{
return CreatePrimitive(EdmPrimitiveTypeKind.Boolean, field);
}
public IEdmTypeReference? Visit(IField<ComponentFieldProperties> field, Args args)
{
return CreateNestedType(field, args.Components.GetSharedFields(field.Properties.SchemaIds, true), args);
}
public IEdmTypeReference? Visit(IField<ComponentsFieldProperties> field, Args args)
{
return CreateNestedType(field, args.Components.GetSharedFields(field.Properties.SchemaIds, true), args);
}
public IEdmTypeReference? Visit(IField<DateTimeFieldProperties> field, Args args)
{
return CreatePrimitive(EdmPrimitiveTypeKind.DateTimeOffset, field);
}
public IEdmTypeReference? Visit(IField<GeolocationFieldProperties> field, Args args)
{
return CreateGeographyPoint(field);
}
public IEdmTypeReference? Visit(IField<JsonFieldProperties> field, Args args)
{
return CreateJson(field);
}
public IEdmTypeReference? Visit(IField<NumberFieldProperties> field, Args args)
{
return CreatePrimitive(EdmPrimitiveTypeKind.Double, field);
}
public IEdmTypeReference? Visit(IField<ReferencesFieldProperties> field, Args args)
{
return CreatePrimitive(EdmPrimitiveTypeKind.String, field);
}
public IEdmTypeReference? Visit(IField<StringFieldProperties> field, Args args)
{
return CreatePrimitive(EdmPrimitiveTypeKind.String, field);
}
public IEdmTypeReference? Visit(IField<TagsFieldProperties> field, Args args)
{
return CreatePrimitive(EdmPrimitiveTypeKind.String, field);
}
public IEdmTypeReference? Visit(IField<UIFieldProperties> field, Args args)
{
return null;
}
private static IEdmTypeReference CreatePrimitive(EdmPrimitiveTypeKind kind, IField field)
{
return EdmCoreModel.Instance.GetPrimitive(kind, !field.RawProperties.IsRequired);
}
private static IEdmTypeReference CreateGeographyPoint(IField<GeolocationFieldProperties> field)
{
return EdmCoreModel.Instance.GetSpatial(EdmPrimitiveTypeKind.GeographyPoint, !field.RawProperties.IsRequired);
}
private static IEdmTypeReference CreateJson(IField<JsonFieldProperties> field)
{
return new EdmComplexTypeReference(JsonType, !field.RawProperties.IsRequired);
}
private IEdmTypeReference? CreateNestedType(IField field, IEnumerable<IField> nested, Args args)
{
if (args.Level > MaxDepth)
{
return null;
}
var (fieldEdmType, created) = args.Factory($"Data.{field.Name.ToPascalCase()}.Nested");
if (created)
{
var nestedArgs = args with { Level = args.Level + 1 };
foreach (var sharedField in nested)
{
var nestedEdmType = sharedField.Accept(this, nestedArgs);
if (nestedEdmType != null)
{
fieldEdmType.AddStructuralProperty(sharedField.Name.EscapeEdmField(), nestedEdmType);
}
}
}
return new EdmComplexTypeReference(fieldEdmType, false);
}
}
}

101
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/AssetQueryModel.cs

@ -0,0 +1,101 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.GenerateFilters
{
public static class AssetQueryModel
{
public static QueryModel Build()
{
var fields = new List<FilterField>
{
new FilterField(FilterSchema.String, "id")
{
Description = FieldDescriptions.EntityId
},
new FilterField(FilterSchema.Boolean, "isDeleted")
{
Description = FieldDescriptions.EntityIsDeleted
},
new FilterField(FilterSchema.DateTime, "created")
{
Description = FieldDescriptions.EntityCreated
},
new FilterField(SharedSchemas.User, "createdBy")
{
Description = FieldDescriptions.EntityCreatedBy
},
new FilterField(FilterSchema.DateTime, "lastModified")
{
Description = FieldDescriptions.EntityLastModified
},
new FilterField(SharedSchemas.User, "lastModifiedBy")
{
Description = FieldDescriptions.EntityLastModifiedBy
},
new FilterField(FilterSchema.String, "status")
{
Description = FieldDescriptions.ContentStatus
},
new FilterField(FilterSchema.String, "version")
{
Description = FieldDescriptions.EntityVersion
},
new FilterField(FilterSchema.String, "fileHash")
{
Description = FieldDescriptions.AssetFileHash
},
new FilterField(FilterSchema.String, "fileName")
{
Description = FieldDescriptions.AssetFileName
},
new FilterField(FilterSchema.Number, "fileSize")
{
Description = FieldDescriptions.AssetFileSize
},
new FilterField(FilterSchema.Number, "fileVersion")
{
Description = FieldDescriptions.AssetFileVersion
},
new FilterField(FilterSchema.Boolean, "isProtected")
{
Description = FieldDescriptions.AssetIsProtected
},
new FilterField(FilterSchema.Any, "metadata")
{
Description = FieldDescriptions.AssetMetadata
},
new FilterField(FilterSchema.String, "mimeType")
{
Description = FieldDescriptions.AssetMimeType
},
new FilterField(FilterSchema.String, "slug")
{
Description = FieldDescriptions.AssetSlug
},
new FilterField(FilterSchema.StringArray, "tags")
{
Description = FieldDescriptions.AssetTags
},
new FilterField(FilterSchema.String, "type")
{
Description = FieldDescriptions.AssetType
}
};
var schema = new FilterSchema(FilterSchemaType.Object)
{
Fields = fields.ToReadonlyList()
};
return new QueryModel { Schema = schema };
}
}
}

76
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/ContentQueryModel.cs

@ -0,0 +1,76 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.GenerateFilters
{
public static class ContentQueryModel
{
public static QueryModel Build(Schema? schema, PartitionResolver partitionResolver, ResolvedComponents components)
{
var fields = new List<FilterField>
{
new FilterField(FilterSchema.String, "id")
{
Description = FieldDescriptions.EntityId
},
new FilterField(FilterSchema.Boolean, "isDeleted")
{
Description = FieldDescriptions.EntityIsDeleted
},
new FilterField(FilterSchema.DateTime, "created")
{
Description = FieldDescriptions.EntityCreated
},
new FilterField(SharedSchemas.User, "createdBy")
{
Description = FieldDescriptions.EntityCreatedBy
},
new FilterField(FilterSchema.DateTime, "lastModified")
{
Description = FieldDescriptions.EntityLastModified
},
new FilterField(SharedSchemas.User, "lastModifiedBy")
{
Description = FieldDescriptions.EntityLastModifiedBy
},
new FilterField(FilterSchema.Number, "version")
{
Description = FieldDescriptions.EntityVersion
},
new FilterField(SharedSchemas.Status, "status")
{
Description = FieldDescriptions.ContentStatus,
},
new FilterField(SharedSchemas.Status, "newStatus", IsNullable: true)
{
Description = FieldDescriptions.ContentNewStatus
}
};
if (schema != null)
{
var dataSchema = schema.BuildDataSchema(partitionResolver, components);
fields.Add(new FilterField(dataSchema, "data")
{
Description = FieldDescriptions.ContentData
});
}
var filterSchema = new FilterSchema(FilterSchemaType.Object)
{
Fields = fields.ToReadonlyList()
};
return new QueryModel { Schema = filterSchema };
}
}
}

86
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterExtensions.cs

@ -0,0 +1,86 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Globalization;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.GenerateFilters
{
public static class JsonSchemaExtensions
{
public static FilterSchema BuildDataSchema(this Schema schema, PartitionResolver partitionResolver,
ResolvedComponents components)
{
Guard.NotNull(partitionResolver, nameof(partitionResolver));
Guard.NotNull(components, nameof(components));
var fields = new List<FilterField>();
var schemaName = schema.DisplayName();
foreach (var field in schema.Fields.ForApi(true))
{
var fieldSchema = FilterVisitor.BuildProperty(field, components);
if (fieldSchema == null)
{
continue;
}
var partitioning = partitionResolver(field.Partitioning);
var partitionFields = new List<FilterField>();
foreach (var partitionKey in partitioning.AllKeys)
{
var partitionDescription = FieldPartitionDescription(field, partitioning.GetName(partitionKey) ?? partitionKey);
var partitionField = new FilterField(
fieldSchema,
partitionKey,
partitionDescription,
true);
partitionFields.Add(partitionField);
}
var filterable = new FilterField(
new FilterSchema(FilterSchemaType.Object)
{
Fields = partitionFields.ToReadonlyList()
},
field.Name,
FieldDescription(schemaName, field));
fields.Add(filterable);
}
var dataSchema = new FilterSchema(FilterSchemaType.Object)
{
Fields = fields.ToReadonlyList()
};
return dataSchema;
}
private static string FieldPartitionDescription(RootField field, string partition)
{
var name = field.DisplayName();
return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ContentPartitionField, name, partition);
}
private static string FieldDescription(string schemaName, RootField field)
{
var name = field.DisplayName();
return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ContentField, name, schemaName);
}
}
}

206
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterVisitor.cs

@ -0,0 +1,206 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Globalization;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Queries;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Core.GenerateFilters
{
internal sealed class FilterVisitor : IFieldVisitor<FilterSchema?, FilterVisitor.Args>
{
private const int MaxDepth = 3;
private static readonly FilterVisitor Instance = new FilterVisitor();
public record struct Args(ResolvedComponents Components, int Level = 0);
private FilterVisitor()
{
}
public static FilterSchema? BuildProperty(IField field, ResolvedComponents components)
{
var args = new Args(components);
return field.Accept(Instance, args);
}
public FilterSchema? Visit(IArrayField field, Args args)
{
if (args.Level >= MaxDepth)
{
return null;
}
var fields = new List<FilterField>();
var nestedArgs = args with { Level = args.Level + 1 };
foreach (var nestedField in field.Fields.ForApi(true))
{
var nestedSchema = nestedField.Accept(this, nestedArgs);
if (nestedSchema != null)
{
var filterableField = new FilterField(
nestedSchema,
nestedField.Name,
ArrayFieldDescription(nestedField),
true);
fields.Add(filterableField);
}
}
return new FilterSchema(FilterSchemaType.ObjectArray)
{
Fields = fields.ToReadonlyList()
};
}
public FilterSchema? Visit(IField<AssetsFieldProperties> field, Args args)
{
return FilterSchema.StringArray;
}
public FilterSchema? Visit(IField<BooleanFieldProperties> field, Args args)
{
return FilterSchema.Boolean;
}
public FilterSchema? Visit(IField<GeolocationFieldProperties> field, Args args)
{
return FilterSchema.GeoObject;
}
public FilterSchema? Visit(IField<JsonFieldProperties> field, Args args)
{
return FilterSchema.Any;
}
public FilterSchema? Visit(IField<NumberFieldProperties> field, Args args)
{
return FilterSchema.Number;
}
public FilterSchema? Visit(IField<StringFieldProperties> field, Args args)
{
return FilterSchema.String;
}
public FilterSchema? Visit(IField<TagsFieldProperties> field, Args args)
{
return FilterSchema.StringArray;
}
public FilterSchema? Visit(IField<UIFieldProperties> field, Args args)
{
return null;
}
public FilterSchema? Visit(IField<ComponentFieldProperties> field, Args args)
{
if (args.Level >= MaxDepth)
{
return null;
}
return new FilterSchema(FilterSchemaType.Object)
{
Fields = BuildComponent(field.Properties.SchemaIds, args)
};
}
public FilterSchema? Visit(IField<ComponentsFieldProperties> field, Args args)
{
if (args.Level >= MaxDepth)
{
return null;
}
return new FilterSchema(FilterSchemaType.Object)
{
Fields = BuildComponent(field.Properties.SchemaIds, args)
};
}
public FilterSchema? Visit(IField<DateTimeFieldProperties> field, Args args)
{
if (field.Properties.Editor == DateTimeFieldEditor.Date)
{
return SharedSchemas.Date;
}
return SharedSchemas.DateTime;
}
public FilterSchema? Visit(IField<ReferencesFieldProperties> field, Args args)
{
return new FilterSchema(FilterSchemaType.StringArray)
{
Extra = new
{
schemaIds = field.Properties.SchemaIds
}
};
}
private ReadonlyList<FilterField> BuildComponent(ReadonlyList<DomainId>? schemaIds, Args args)
{
var fields = new List<FilterField>();
var nestedArgs = args with { Level = args.Level + 1 };
foreach (var schema in args.Components.Resolve(schemaIds).Values)
{
var componentName = schema.DisplayName();
foreach (var field in schema.Fields.ForApi(true))
{
var fieldSchema = field.Accept(this, nestedArgs);
if (fieldSchema != null)
{
var filterableField = new FilterField(
fieldSchema,
field.Name,
ComponentFieldDescription(componentName, field),
true);
fields.Add(filterableField);
}
}
fields.Add(new FilterField(FilterSchema.String, Component.Discriminator)
{
Description = FieldDescriptions.ComponentSchemaId
});
}
return fields.ToReadonlyList();
}
private static string ArrayFieldDescription(IField field)
{
var name = field.DisplayName();
return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ContentArrayField, name);
}
private static string ComponentFieldDescription(string componentName, RootField field)
{
var name = field.DisplayName();
return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ComponentField, name, componentName);
}
}
}

46
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/SharedSchemas.cs

@ -0,0 +1,46 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.GenerateFilters
{
internal static class SharedSchemas
{
public static readonly FilterSchema Date = new FilterSchema(FilterSchemaType.DateTime)
{
Extra = new
{
editor = "Date"
}
};
public static readonly FilterSchema DateTime = new FilterSchema(FilterSchemaType.DateTime)
{
Extra = new
{
editor = "DateTime"
}
};
public static readonly FilterSchema Status = new FilterSchema(FilterSchemaType.String)
{
Extra = new
{
editor = "Status"
}
};
public static readonly FilterSchema User = new FilterSchema(FilterSchemaType.String)
{
Extra = new
{
editor = "User"
}
};
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs → backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentJsonSchema.cs

@ -9,9 +9,9 @@ using NJsonSchema;
namespace Squidex.Domain.Apps.Core.GenerateJsonSchema namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{ {
public static class ContentJsonSchemaBuilder public static class ContentJsonSchema
{ {
public static JsonSchema BuildSchema(JsonSchema? dataSchema, bool extended = false, bool withDeleted = false) public static JsonSchema Build(JsonSchema? dataSchema, bool extended = false, bool withDeleted = false)
{ {
var jsonSchema = new JsonSchema var jsonSchema = new JsonSchema
{ {

4
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeBuilder.cs

@ -52,11 +52,11 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
.SetRequired(isRequired); .SetRequired(isRequired);
} }
public static JsonSchemaProperty StringProperty(string? description = null, bool isRequired = false) public static JsonSchemaProperty StringProperty(string? description = null, bool isRequired = false, string? format = null)
{ {
const JsonObjectType type = JsonObjectType.String; const JsonObjectType type = JsonObjectType.String;
return new JsonSchemaProperty { Type = type } return new JsonSchemaProperty { Type = type, Format = format }
.SetDescription(description) .SetDescription(description)
.SetRequired(isRequired); .SetRequired(isRequired);
} }

19
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs

@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
private const int MaxDepth = 5; private const int MaxDepth = 5;
private static readonly JsonTypeVisitor Instance = new JsonTypeVisitor(); private static readonly JsonTypeVisitor Instance = new JsonTypeVisitor();
public sealed record Args(ResolvedComponents Components, Schema Schema, public record struct Args(ResolvedComponents Components, Schema Schema,
JsonTypeFactory Factory, JsonTypeFactory Factory,
bool WithHidden, bool WithHidden,
bool WithComponents, bool WithComponents,
@ -145,7 +145,16 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public JsonSchemaProperty? Visit(IField<ReferencesFieldProperties> field, Args args) public JsonSchemaProperty? Visit(IField<ReferencesFieldProperties> field, Args args)
{ {
return JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String()); var property = JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String());
property.Format = GeoJson.Format;
property.ExtensionData = new Dictionary<string, object>
{
["schemaIds"] = field.Properties.SchemaIds ?? ReadonlyList.Empty<DomainId>()
};
return property;
} }
public JsonSchemaProperty? Visit(IField<StringFieldProperties> field, Args args) public JsonSchemaProperty? Visit(IField<StringFieldProperties> field, Args args)
@ -223,7 +232,11 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
} }
jsonSchema.DiscriminatorObject = discriminator; jsonSchema.DiscriminatorObject = discriminator;
jsonSchema.Properties.Add(Component.Discriminator, JsonTypeBuilder.StringProperty(isRequired: true));
if (discriminator.Mapping.Count > 0)
{
jsonSchema.Properties.Add(Component.Discriminator, JsonTypeBuilder.StringProperty(isRequired: true));
}
} }
else else
{ {

27
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs

@ -6,13 +6,14 @@
// ========================================================================== // ==========================================================================
using Jint.Native; using Jint.Native;
using Squidex.Domain.Apps.Core.Properties;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Text; using Squidex.Text;
namespace Squidex.Domain.Apps.Core.HandleRules.Extensions namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
{ {
public sealed class EventJintExtension : IJintExtension public sealed class EventJintExtension : IJintExtension, IScriptDescriptor
{ {
private delegate JsValue EventDelegate(); private delegate JsValue EventDelegate();
private readonly IUrlGenerator urlGenerator; private readonly IUrlGenerator urlGenerator;
@ -74,5 +75,29 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
return JsValue.Null; return JsValue.Null;
})); }));
} }
public void Describe(AddDescription describe, ScriptScope scope)
{
if ((scope & ScriptScope.ContentTrigger) == ScriptScope.ContentTrigger)
{
describe(JsonType.Function, "contentAction",
Resources.ScriptingContentAction);
describe(JsonType.Function, "contentUrl",
Resources.ScriptingContentUrl);
}
if ((scope & ScriptScope.AssetTrigger) == ScriptScope.AssetTrigger)
{
describe(JsonType.Function, "assetContentUrl",
Resources.ScriptingAssetContentUrl);
describe(JsonType.Function, "assetContentAppUrl",
Resources.ScriptingAssetContentAppUrl);
describe(JsonType.Function, "assetContentSlugUrl",
Resources.ScriptingAssetContentSlugUrl);
}
}
} }
} }

306
backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs

@ -0,0 +1,306 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Squidex.Domain.Apps.Core.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Core.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to The download URL to the asset..
/// </summary>
internal static string ScriptingAssetContentAppUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentAppUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The download URL to the asset using the file slug instead of the ID..
/// </summary>
internal static string ScriptingAssetContentSlugUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentSlugUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The download URL to the asset without the app name (deprecated)..
/// </summary>
internal static string ScriptingAssetContentUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text..
/// </summary>
internal static string ScriptingCharacterCount {
get {
return ResourceManager.GetString("ScriptingCharacterCount", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Completes the script when an async method is used..
/// </summary>
internal static string ScriptingComplete {
get {
return ResourceManager.GetString("ScriptingComplete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The status of the content..
/// </summary>
internal static string ScriptingContentAction {
get {
return ResourceManager.GetString("ScriptingContentAction", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The URL to the content in the UI..
/// </summary>
internal static string ScriptingContentUrl {
get {
return ResourceManager.GetString("ScriptingContentUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingDeleteJson {
get {
return ResourceManager.GetString("ScriptingDeleteJson", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tell Squidex to not allow the current operation and to return a 403 (Forbidden)..
/// </summary>
internal static string ScriptingDisallow {
get {
return ResourceManager.GetString("ScriptingDisallow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern..
/// </summary>
internal static string ScriptingFormatDate {
get {
return ResourceManager.GetString("ScriptingFormatDate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern..
/// </summary>
internal static string ScriptingFormatTime {
get {
return ResourceManager.GetString("ScriptingFormatTime", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Makes a GET request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingGetJSON {
get {
return ResourceManager.GetString("ScriptingGetJSON", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Generates a guid..
/// </summary>
internal static string ScriptingGuid {
get {
return ResourceManager.GetString("ScriptingGuid", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Converts a HTML string to plain text..
/// </summary>
internal static string ScriptingHtml2Text {
get {
return ResourceManager.GetString("ScriptingHtml2Text", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Converts a markdown string to plain text..
/// </summary>
internal static string ScriptingMarkdown2Text {
get {
return ResourceManager.GetString("ScriptingMarkdown2Text", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important..
/// </summary>
internal static string ScriptingMD5 {
get {
return ResourceManager.GetString("ScriptingMD5", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPatchJson {
get {
return ResourceManager.GetString("ScriptingPatchJson", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Makes a POST request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPostJSON {
get {
return ResourceManager.GetString("ScriptingPostJSON", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPutJson {
get {
return ResourceManager.GetString("ScriptingPutJson", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tell Squidex to reject the current operation and to return a 403 (Forbidden)..
/// </summary>
internal static string ScriptingReject {
get {
return ResourceManager.GetString("ScriptingReject", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tell Squidex that you have modified the data and that the change should be applied..
/// </summary>
internal static string ScriptingReplace {
get {
return ResourceManager.GetString("ScriptingReplace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords..
/// </summary>
internal static string ScriptingSHA256 {
get {
return ResourceManager.GetString("ScriptingSHA256", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords..
/// </summary>
internal static string ScriptingSHA512 {
get {
return ResourceManager.GetString("ScriptingSHA512", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs..
/// </summary>
internal static string ScriptingSlugify {
get {
return ResourceManager.GetString("ScriptingSlugify", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Converts a text to camelCase..
/// </summary>
internal static string ScriptingToCamelCase {
get {
return ResourceManager.GetString("ScriptingToCamelCase", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Converts a text to PascalCase.
/// </summary>
internal static string ScriptingToPascalCase {
get {
return ResourceManager.GetString("ScriptingToPascalCase", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Counts the number of words in a text. Useful in combination with html2Text or markdown2Text..
/// </summary>
internal static string ScriptingWordCount {
get {
return ResourceManager.GetString("ScriptingWordCount", resourceCulture);
}
}
}
}

201
backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ScriptingAssetContentAppUrl" xml:space="preserve">
<value>The download URL to the asset.</value>
</data>
<data name="ScriptingAssetContentSlugUrl" xml:space="preserve">
<value>The download URL to the asset using the file slug instead of the ID.</value>
</data>
<data name="ScriptingAssetContentUrl" xml:space="preserve">
<value>The download URL to the asset without the app name (deprecated).</value>
</data>
<data name="ScriptingCharacterCount" xml:space="preserve">
<value>Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text.</value>
</data>
<data name="ScriptingComplete" xml:space="preserve">
<value>Completes the script when an async method is used.</value>
</data>
<data name="ScriptingContentAction" xml:space="preserve">
<value>The status of the content.</value>
</data>
<data name="ScriptingContentUrl" xml:space="preserve">
<value>The URL to the content in the UI.</value>
</data>
<data name="ScriptingDeleteJson" xml:space="preserve">
<value>Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional.</value>
</data>
<data name="ScriptingDisallow" xml:space="preserve">
<value>Tell Squidex to not allow the current operation and to return a 403 (Forbidden).</value>
</data>
<data name="ScriptingFormatDate" xml:space="preserve">
<value>Formats a JavaScript date object using the specified pattern.</value>
</data>
<data name="ScriptingFormatTime" xml:space="preserve">
<value>Formats a JavaScript date object using the specified pattern.</value>
</data>
<data name="ScriptingGetJSON" xml:space="preserve">
<value>Makes a GET request to the defined URL and parses the result as JSON. Headers are optional.</value>
</data>
<data name="ScriptingGuid" xml:space="preserve">
<value>Generates a guid.</value>
</data>
<data name="ScriptingHtml2Text" xml:space="preserve">
<value>Converts a HTML string to plain text.</value>
</data>
<data name="ScriptingMarkdown2Text" xml:space="preserve">
<value>Converts a markdown string to plain text.</value>
</data>
<data name="ScriptingMD5" xml:space="preserve">
<value>Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important.</value>
</data>
<data name="ScriptingPatchJson" xml:space="preserve">
<value>Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional.</value>
</data>
<data name="ScriptingPostJSON" xml:space="preserve">
<value>Makes a POST request to the defined URL and parses the result as JSON. Headers are optional.</value>
</data>
<data name="ScriptingPutJson" xml:space="preserve">
<value>Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional.</value>
</data>
<data name="ScriptingReject" xml:space="preserve">
<value>Tell Squidex to reject the current operation and to return a 403 (Forbidden).</value>
</data>
<data name="ScriptingReplace" xml:space="preserve">
<value>Tell Squidex that you have modified the data and that the change should be applied.</value>
</data>
<data name="ScriptingSHA256" xml:space="preserve">
<value>Calculate the SHA256 hash from a given string. Use this method for hashing passwords.</value>
</data>
<data name="ScriptingSHA512" xml:space="preserve">
<value>Calculate the SHA256 hash from a given string. Use this method for hashing passwords.</value>
</data>
<data name="ScriptingSlugify" xml:space="preserve">
<value>Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.</value>
</data>
<data name="ScriptingToCamelCase" xml:space="preserve">
<value>Converts a text to camelCase.</value>
</data>
<data name="ScriptingToPascalCase" xml:space="preserve">
<value>Converts a text to PascalCase</value>
</data>
<data name="ScriptingWordCount" xml:space="preserve">
<value>Counts the number of words in a text. Useful in combination with html2Text or markdown2Text.</value>
</data>
</root>

12
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeJintExtension.cs

@ -8,10 +8,11 @@
using System.Globalization; using System.Globalization;
using Jint; using Jint;
using Jint.Native; using Jint.Native;
using Squidex.Domain.Apps.Core.Properties;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{ {
public sealed class DateTimeJintExtension : IJintExtension public sealed class DateTimeJintExtension : IJintExtension, IScriptDescriptor
{ {
private readonly Func<DateTime, string, JsValue> formatDate = (date, format) => private readonly Func<DateTime, string, JsValue> formatDate = (date, format) =>
{ {
@ -30,5 +31,14 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
engine.SetValue("formatTime", formatDate); engine.SetValue("formatTime", formatDate);
engine.SetValue("formatDate", formatDate); engine.SetValue("formatDate", formatDate);
} }
public void Describe(AddDescription describe, ScriptScope scope)
{
describe(JsonType.Function, "formatDate(data, pattern)",
Resources.ScriptingFormatDate);
describe(JsonType.Function, "formatTime(text)",
Resources.ScriptingFormatTime);
}
} }
} }

26
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs

@ -10,11 +10,12 @@ using Jint;
using Jint.Native; using Jint.Native;
using Jint.Native.Json; using Jint.Native.Json;
using Jint.Runtime; using Jint.Runtime;
using Squidex.Domain.Apps.Core.Properties;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{ {
public sealed class HttpJintExtension : IJintExtension public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor
{ {
private delegate void HttpJson(string url, Action<JsValue> callback, JsValue? headers = null); private delegate void HttpJson(string url, Action<JsValue> callback, JsValue? headers = null);
private delegate void HttpJsonWithBody(string url, JsValue post, Action<JsValue> callback, JsValue? headers = null); private delegate void HttpJsonWithBody(string url, JsValue post, Action<JsValue> callback, JsValue? headers = null);
@ -27,12 +28,29 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
public void ExtendAsync(ScriptExecutionContext context) public void ExtendAsync(ScriptExecutionContext context)
{ {
AddMethod(context, HttpMethod.Get, "getJSON");
AddMethod(context, HttpMethod.Delete, "deleteJSON");
AdBodyMethod(context, HttpMethod.Patch, "patchJSON"); AdBodyMethod(context, HttpMethod.Patch, "patchJSON");
AdBodyMethod(context, HttpMethod.Post, "postJSON"); AdBodyMethod(context, HttpMethod.Post, "postJSON");
AdBodyMethod(context, HttpMethod.Put, "putJSON"); AdBodyMethod(context, HttpMethod.Put, "putJSON");
AddMethod(context, HttpMethod.Delete, "deleteJSON");
AddMethod(context, HttpMethod.Get, "getJSON");
}
public void Describe(AddDescription describe, ScriptScope scope)
{
describe(JsonType.Function, "getJSON(url, callback, ?headers)",
Resources.ScriptingGetJSON);
describe(JsonType.Function, "postJSON(url, body, callback, ?headers)",
Resources.ScriptingPostJSON);
describe(JsonType.Function, "putJSON(url, body, callback, ?headers)",
Resources.ScriptingPutJson);
describe(JsonType.Function, "patchJSON(url, body, callback, headers)",
Resources.ScriptingPatchJson);
describe(JsonType.Function, "deleteJSON(url, body, callback, headers)",
Resources.ScriptingDeleteJson);
} }
private void AddMethod(ScriptExecutionContext context, HttpMethod method, string name) private void AddMethod(ScriptExecutionContext context, HttpMethod method, string name)

42
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs

@ -7,12 +7,13 @@
using Jint; using Jint;
using Jint.Native; using Jint.Native;
using Squidex.Domain.Apps.Core.Properties;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Text; using Squidex.Text;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{ {
public sealed class StringJintExtension : IJintExtension public sealed class StringJintExtension : IJintExtension, IScriptDescriptor
{ {
private delegate JsValue StringSlugifyDelegate(string text, bool single = false); private delegate JsValue StringSlugifyDelegate(string text, bool single = false);
@ -121,20 +122,45 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
public void Extend(Engine engine) public void Extend(Engine engine)
{ {
engine.SetValue("slugify", slugify);
engine.SetValue("guid", guid); engine.SetValue("guid", guid);
engine.SetValue("html2Text", Html2Text);
engine.SetValue("markdown2Text", markdown2Text);
engine.SetValue("md5", md5);
engine.SetValue("sha256", sha256); engine.SetValue("sha256", sha256);
engine.SetValue("sha512", sha512); engine.SetValue("sha512", sha512);
engine.SetValue("md5", md5); engine.SetValue("slugify", slugify);
engine.SetValue("toCamelCase", toCamelCase); engine.SetValue("toCamelCase", toCamelCase);
engine.SetValue("toPascalCase", toPascalCase); engine.SetValue("toPascalCase", toPascalCase);
}
engine.SetValue("html2Text", Html2Text); public void Describe(AddDescription describe, ScriptScope scope)
{
describe(JsonType.Function, "html2Text(text)",
Resources.ScriptingHtml2Text);
engine.SetValue("markdown2Text", markdown2Text); describe(JsonType.Function, "markdown2Text(text)",
Resources.ScriptingMarkdown2Text);
describe(JsonType.Function, "toCamelCase(text)",
Resources.ScriptingToCamelCase);
describe(JsonType.Function, "toPascalCase(text)",
Resources.ScriptingToPascalCase);
describe(JsonType.Function, "md5(text)",
Resources.ScriptingMD5);
describe(JsonType.Function, "sha256(text)",
Resources.ScriptingSHA256);
describe(JsonType.Function, "sha512(text)",
Resources.ScriptingSHA512);
describe(JsonType.Function, "slugify(text)",
Resources.ScriptingSlugify);
describe(JsonType.Function, "guid()",
Resources.ScriptingGuid);
} }
} }
} }

13
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringWordsJintExtension.cs

@ -7,11 +7,12 @@
using Jint; using Jint;
using Jint.Native; using Jint.Native;
using Squidex.Domain.Apps.Core.Properties;
using Squidex.Text; using Squidex.Text;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{ {
public sealed class StringWordsJintExtension : IJintExtension public sealed class StringWordsJintExtension : IJintExtension, IScriptDescriptor
{ {
private readonly Func<string, JsValue> wordCount = text => private readonly Func<string, JsValue> wordCount = text =>
{ {
@ -40,8 +41,16 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
public void Extend(Engine engine) public void Extend(Engine engine)
{ {
engine.SetValue("wordCount", wordCount); engine.SetValue("wordCount", wordCount);
engine.SetValue("characterCount", characterCount); engine.SetValue("characterCount", characterCount);
} }
public void Describe(AddDescription describe, ScriptScope scope)
{
describe(JsonType.Function, "wordCount(text)",
Resources.ScriptingWordCount);
describe(JsonType.Function, "characterCount(text)",
Resources.ScriptingCharacterCount);
}
} }
} }

16
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptDescriptor.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Scripting
{
public delegate void AddDescription(JsonType type, string name, string description);
public interface IScriptDescriptor
{
void Describe(AddDescription describe, ScriptScope scope);
}
}

21
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -12,6 +12,7 @@ using Jint.Runtime;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Properties;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Squidex.Domain.Apps.Core.Scripting.Internal; using Squidex.Domain.Apps.Core.Scripting.Internal;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -21,7 +22,7 @@ using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Core.Scripting namespace Squidex.Domain.Apps.Core.Scripting
{ {
public sealed class JintScriptEngine : IScriptEngine public sealed class JintScriptEngine : IScriptEngine, IScriptDescriptor
{ {
private readonly IJintExtension[] extensions; private readonly IJintExtension[] extensions;
private readonly Parser parser; private readonly Parser parser;
@ -208,5 +209,23 @@ namespace Squidex.Domain.Apps.Core.Scripting
throw new ValidationException(T.Get("common.jsError", new { message = ex.GetType().Name }), ex); throw new ValidationException(T.Get("common.jsError", new { message = ex.GetType().Name }), ex);
} }
} }
public void Describe(AddDescription describe, ScriptScope scope)
{
if (scope == ScriptScope.Transform)
{
describe(JsonType.Function, "replace()",
Resources.ScriptingReplace);
}
describe(JsonType.Function, "disallow()",
Resources.ScriptingDisallow);
describe(JsonType.Function, "complete()",
Resources.ScriptingComplete);
describe(JsonType.Function, "reject(reason)",
Resources.ScriptingReject);
}
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Scripting/JsonType.cs → backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JsonType.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
namespace Squidex.Domain.Apps.Entities.Scripting namespace Squidex.Domain.Apps.Core.Scripting
{ {
public enum JsonType public enum JsonType
{ {

19
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptScope.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Scripting
{
[Flags]
public enum ScriptScope
{
AssetScript,
AssetTrigger,
ContentScript,
ContentTrigger,
Transform
}
}

307
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs

@ -0,0 +1,307 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptingCompleter
{
private readonly IEnumerable<IScriptDescriptor> descriptors;
public ScriptingCompleter(IEnumerable<IScriptDescriptor> descriptors)
{
this.descriptors = descriptors;
}
public IReadOnlyList<ScriptingValue> ContentScript(FilterSchema dataSchema)
{
Guard.NotNull(dataSchema, nameof(dataSchema));
return new Process(descriptors).Content(dataSchema, ScriptScope.ContentScript | ScriptScope.Transform);
}
public IReadOnlyList<ScriptingValue> ContentTrigger(FilterSchema dataSchema)
{
Guard.NotNull(dataSchema, nameof(dataSchema));
return new Process(descriptors).Content(dataSchema, ScriptScope.ContentTrigger);
}
public IReadOnlyList<ScriptingValue> AssetScript()
{
return new Process(descriptors).Asset(ScriptScope.AssetScript);
}
public IReadOnlyList<ScriptingValue> AssetTrigger()
{
return new Process(descriptors).Asset(ScriptScope.AssetTrigger);
}
private sealed class Process
{
private readonly Stack<string> prefixes = new Stack<string>();
private readonly HashSet<ScriptingValue> result = new HashSet<ScriptingValue>();
private readonly IEnumerable<IScriptDescriptor> descriptors;
public Process(IEnumerable<IScriptDescriptor> descriptors)
{
this.descriptors = descriptors;
}
public IReadOnlyList<ScriptingValue> Content(FilterSchema dataSchema, ScriptScope scope)
{
AddShared(scope);
AddObject("ctx", FieldDescriptions.Context, () =>
{
AddString("contentId",
FieldDescriptions.EntityId);
AddString("status",
FieldDescriptions.ContentStatus);
AddString("statusOld",
FieldDescriptions.ContentStatusOld);
AddObject("data", FieldDescriptions.ContentData, () =>
{
AddData(dataSchema);
});
AddObject("dataOld", FieldDescriptions.ContentDataOld, () =>
{
AddData(dataSchema);
});
});
return result.OrderBy(x => x.Path).ToList();
}
public IReadOnlyList<ScriptingValue> Asset(ScriptScope scope)
{
AddShared(scope);
AddObject("ctx", FieldDescriptions.Context, () =>
{
AddString("assetId",
FieldDescriptions.EntityId);
AddObject("asset",
FieldDescriptions.Asset, () =>
{
AddSharedAsset();
AddNumber("fileVersion",
FieldDescriptions.AssetFileVersion);
});
AddObject("command",
FieldDescriptions.Command, () =>
{
AddSharedAsset();
AddBoolean("permanent",
FieldDescriptions.EntityRequestDeletePermanent);
});
});
return result.OrderBy(x => x.Path).ToList();
}
private void AddSharedAsset()
{
AddString("fileHash",
FieldDescriptions.AssetFileHash);
AddString("fileName",
FieldDescriptions.AssetFileName);
AddString("fileSize",
FieldDescriptions.AssetFileSize);
AddString("fileSlug",
FieldDescriptions.AssetSlug);
AddString("mimeType",
FieldDescriptions.AssetMimeType);
AddBoolean("isProtected",
FieldDescriptions.AssetIsProtected);
AddString("parentId",
FieldDescriptions.AssetParentId);
AddArray("parentPath",
FieldDescriptions.AssetParentPath);
AddArray("tags",
FieldDescriptions.AssetTags);
AddObject("metadata",
FieldDescriptions.AssetMetadata, () =>
{
AddArray("name",
FieldDescriptions.AssetMetadataValue);
});
}
private void AddShared(ScriptScope scope)
{
foreach (var descriptor in descriptors)
{
descriptor.Describe(Add, scope);
}
AddString("appId",
FieldDescriptions.AppId);
AddString("appName",
FieldDescriptions.AppName);
AddString("operation",
FieldDescriptions.Operation);
AddObject("user",
FieldDescriptions.User, () =>
{
AddString("id",
FieldDescriptions.UserId);
AddString("email",
FieldDescriptions.UserEmail);
AddBoolean("isClient",
FieldDescriptions.UserIsClient);
AddBoolean("isUser",
FieldDescriptions.UserIsUser);
AddObject("claims",
FieldDescriptions.UserClaims, () =>
{
AddArray("name",
FieldDescriptions.UsersClaimsValue);
});
});
}
private void AddData(FilterSchema dataSchema)
{
if (dataSchema.Fields == null)
{
return;
}
foreach (var field in dataSchema.Fields)
{
switch (field.Schema.Type)
{
case FilterSchemaType.Any:
AddAny(field.Path, field.Description);
break;
case FilterSchemaType.Boolean:
AddBoolean(field.Path, field.Description);
break;
case FilterSchemaType.DateTime:
AddString(field.Path, field.Description);
break;
case FilterSchemaType.GeoObject:
AddObject(field.Path, field.Description);
break;
case FilterSchemaType.Guid:
AddString(field.Path, field.Description);
break;
case FilterSchemaType.Number:
AddNumber(field.Path, field.Description);
break;
case FilterSchemaType.Object:
AddObject(field.Path, field.Description);
break;
case FilterSchemaType.ObjectArray:
AddArray(field.Path, field.Description);
break;
case FilterSchemaType.String:
AddString(field.Path, field.Description);
break;
case FilterSchemaType.StringArray:
AddArray(field.Path, field.Description);
break;
}
}
}
private void AddAny(string? name, string? description)
{
Add(JsonType.Any, name, description);
}
private void AddArray(string? name, string? description)
{
Add(JsonType.Array, name, description);
}
private void AddBoolean(string? name, string? description)
{
Add(JsonType.Boolean, name, description);
}
private void AddObject(string? name, string? description)
{
Add(JsonType.Object, name, description);
}
private void AddNumber(string? name, string? description)
{
Add(JsonType.Number, name, description);
}
private void AddString(string? name, string? description)
{
Add(JsonType.String, name, description);
}
private void Add(JsonType type, string? name, string? description)
{
if (name != null)
{
prefixes.Push(name);
}
if (prefixes.Count == 0)
{
return;
}
var path = string.Join('.', prefixes.Reverse());
result.Add(new ScriptingValue(path, type, description));
if (name != null)
{
prefixes.Pop();
}
}
private void AddObject(string name, string description, Action inner)
{
Add(JsonType.Object, name, description);
prefixes.Push(name);
try
{
inner();
}
finally
{
prefixes.Pop();
}
}
}
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptingValue.cs → backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingValue.cs

@ -7,9 +7,9 @@
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Entities.Scripting namespace Squidex.Domain.Apps.Core.Scripting
{ {
public sealed record ScriptingValue(string Path, JsonType Type, string Description) public sealed record ScriptingValue(string Path, JsonType Type, string? Description)
{ {
} }
} }

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

@ -18,14 +18,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Fluid.Core.Squidex" Version="1.0.0-beta" /> <PackageReference Include="Fluid.Core.Squidex" Version="1.0.0-beta" />
<PackageReference Include="GeoJSON.Net" Version="1.2.19" /> <PackageReference Include="GeoJSON.Net" Version="1.2.19" />
<PackageReference Include="Jint" Version="3.0.0-beta-2036" /> <PackageReference Include="Jint" Version="3.0.0-beta-2036" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.680"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.680">
<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="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.9.4" />
<PackageReference Include="NJsonSchema" Version="10.6.6" /> <PackageReference Include="NJsonSchema" Version="10.6.6" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
@ -36,4 +35,17 @@
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> <AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project> </Project>

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

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
private static readonly DefaultFieldValueValidatorsFactory Instance = new DefaultFieldValueValidatorsFactory(); private static readonly DefaultFieldValueValidatorsFactory Instance = new DefaultFieldValueValidatorsFactory();
public sealed record Args(ValidatorContext Context, ValidatorFactory Factory); public record struct Args(ValidatorContext Context, ValidatorFactory Factory);
private DefaultFieldValueValidatorsFactory() private DefaultFieldValueValidatorsFactory()
{ {

2
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs

@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
private static readonly JsonValueConverter Instance = new JsonValueConverter(); private static readonly JsonValueConverter Instance = new JsonValueConverter();
public sealed record Args(IJsonValue Value, IJsonSerializer JsonSerializer, ResolvedComponents Components); public record struct Args(IJsonValue Value, IJsonSerializer JsonSerializer, ResolvedComponents Components);
private JsonValueConverter() private JsonValueConverter()
{ {

2
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
private static readonly JsonValueValidator Instance = new JsonValueValidator(); private static readonly JsonValueValidator Instance = new JsonValueValidator();
public sealed record Args(IJsonValue Value, IJsonSerializer JsonSerializer); public record struct Args(IJsonValue Value, IJsonSerializer JsonSerializer);
private JsonValueValidator() private JsonValueValidator()
{ {

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/AdaptIdVisitor.cs

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb
{ {
private static readonly AdaptIdVisitor Instance = new AdaptIdVisitor(); private static readonly AdaptIdVisitor Instance = new AdaptIdVisitor();
public sealed record Args(DomainId AppId); public record struct Args(DomainId AppId);
private AdaptIdVisitor() private AdaptIdVisitor()
{ {

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs

@ -6,10 +6,10 @@
// ========================================================================== // ==========================================================================
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.OData;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {

15
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs

@ -14,12 +14,13 @@ using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Properties;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetsJintExtension : IJintExtension public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
{ {
private delegate void GetAssetsDelegate(JsValue references, Action<JsValue> callback); private delegate void GetAssetsDelegate(JsValue references, Action<JsValue> callback);
private delegate void GetAssetTextDelegate(JsValue references, Action<JsValue> callback, JsValue encoding); private delegate void GetAssetTextDelegate(JsValue references, Action<JsValue> callback, JsValue encoding);
@ -36,6 +37,18 @@ namespace Squidex.Domain.Apps.Entities.Assets
AddAsset(context); AddAsset(context);
} }
public void Describe(AddDescription describe, ScriptScope scope)
{
describe(JsonType.Function, "getAssets(ids, callback)",
Resources.ScriptingGetAssets);
describe(JsonType.Function, "getAsset(ids, callback)",
Resources.ScriptingGetAsset);
describe(JsonType.Function, "getAssetText(asset, callback, encoding)",
Resources.ScriptingGetAssetText);
}
private void AddAsset(ScriptExecutionContext context) private void AddAsset(ScriptExecutionContext context)
{ {
if (!context.TryGetValue<DomainId>("appId", out var appId)) if (!context.TryGetValue<DomainId>("appId", out var appId))

91
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs

@ -9,6 +9,7 @@ using Microsoft.Extensions.Options;
using Microsoft.OData; using Microsoft.OData;
using Microsoft.OData.Edm; using Microsoft.OData.Edm;
using NJsonSchema; using NJsonSchema;
using Squidex.Domain.Apps.Core.GenerateFilters;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
@ -17,14 +18,13 @@ using Squidex.Infrastructure.Queries.Json;
using Squidex.Infrastructure.Queries.OData; using Squidex.Infrastructure.Queries.OData;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
using Squidex.Text;
namespace Squidex.Domain.Apps.Entities.Assets.Queries namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
public class AssetQueryParser public class AssetQueryParser
{ {
private readonly JsonSchema jsonSchema = BuildJsonSchema(); private readonly QueryModel queryModel = AssetQueryModel.Build();
private readonly IEdmModel edmModel = BuildEdmModel(); private readonly IEdmModel edmModel;
private readonly IJsonSerializer jsonSerializer; private readonly IJsonSerializer jsonSerializer;
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly AssetOptions options; private readonly AssetOptions options;
@ -32,10 +32,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
public AssetQueryParser(IJsonSerializer jsonSerializer, ITagService tagService, IOptions<AssetOptions> options) public AssetQueryParser(IJsonSerializer jsonSerializer, ITagService tagService, IOptions<AssetOptions> options)
{ {
this.jsonSerializer = jsonSerializer; this.jsonSerializer = jsonSerializer;
this.tagService = tagService; this.tagService = tagService;
this.options = options.Value; this.options = options.Value;
edmModel = queryModel.ConvertToEdm("Squidex", "Asset");
} }
public virtual async Task<Q> ParseAsync(Context context, Q q) public virtual async Task<Q> ParseAsync(Context context, Q q)
@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
private ClrQuery ParseJson(string json) private ClrQuery ParseJson(string json)
{ {
return jsonSchema.Parse(json, jsonSerializer); return queryModel.Parse(json, jsonSerializer);
} }
private ClrQuery ParseOData(string odata) private ClrQuery ParseOData(string odata)
@ -149,84 +149,5 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
throw new ValidationException(T.Get("common.odataNotSupported", new { odata })); throw new ValidationException(T.Get("common.odataNotSupported", new { odata }));
} }
} }
private static JsonSchema BuildJsonSchema()
{
var schema = new JsonSchema { Title = "Asset", Type = JsonObjectType.Object };
void AddProperty(string name, JsonObjectType type, string? format = null)
{
var property = new JsonSchemaProperty { Type = type, Format = format };
schema.Properties[name.ToCamelCase()] = property;
}
AddProperty("id", JsonObjectType.String);
AddProperty("version", JsonObjectType.Integer);
AddProperty("created", JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty("createdBy", JsonObjectType.String);
AddProperty("fileHash", JsonObjectType.String);
AddProperty("fileName", JsonObjectType.String);
AddProperty("fileSize", JsonObjectType.Integer);
AddProperty("fileVersion", JsonObjectType.Integer);
AddProperty("isDeleted", JsonObjectType.Boolean);
AddProperty("isProtected", JsonObjectType.Boolean);
AddProperty("lastModified", JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty("lastModifiedBy", JsonObjectType.String);
AddProperty("metadata", JsonObjectType.None);
AddProperty("mimeType", JsonObjectType.String);
AddProperty("slug", JsonObjectType.String);
AddProperty("tags", JsonObjectType.String);
AddProperty("type", JsonObjectType.String);
return schema;
}
private static IEdmModel BuildEdmModel()
{
var entityType = new EdmEntityType("Squidex", "Asset");
void AddProperty(string name, EdmPrimitiveTypeKind type)
{
entityType.AddStructuralProperty(name.ToCamelCase(), type);
}
void AddPropertyReference(string name, IEdmTypeReference reference)
{
entityType.AddStructuralProperty(name.ToCamelCase(), reference);
}
var jsonType = new EdmComplexType("Squidex", "Json", null, false, true);
AddPropertyReference("Metadata", new EdmComplexTypeReference(jsonType, false));
AddProperty("id", EdmPrimitiveTypeKind.String);
AddProperty("version", EdmPrimitiveTypeKind.Int64);
AddProperty("created", EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty("createdBy", EdmPrimitiveTypeKind.String);
AddProperty("fileHash", EdmPrimitiveTypeKind.String);
AddProperty("fileName", EdmPrimitiveTypeKind.String);
AddProperty("isDeleted", EdmPrimitiveTypeKind.Boolean);
AddProperty("isProtected", EdmPrimitiveTypeKind.Boolean);
AddProperty("fileSize", EdmPrimitiveTypeKind.Int64);
AddProperty("fileVersion", EdmPrimitiveTypeKind.Int64);
AddProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty("lastModifiedBy", EdmPrimitiveTypeKind.String);
AddProperty("mimeType", EdmPrimitiveTypeKind.String);
AddProperty("slug", EdmPrimitiveTypeKind.String);
AddProperty("tags", EdmPrimitiveTypeKind.String);
AddProperty("type", EdmPrimitiveTypeKind.String);
var container = new EdmEntityContainer("Squidex", "Container");
container.AddEntitySet("AssetSet", entityType);
var model = new EdmModel();
model.AddElement(container);
model.AddElement(entityType);
return model;
}
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
private static readonly FilterTagTransformer Instance = new FilterTagTransformer(); private static readonly FilterTagTransformer Instance = new FilterTagTransformer();
public sealed record Args(DomainId AppId, ITagService TagService); public record struct Args(DomainId AppId, ITagService TagService);
private FilterTagTransformer() private FilterTagTransformer()
{ {

12
backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs

@ -7,12 +7,13 @@
using Orleans; using Orleans;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Properties;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents.Counter namespace Squidex.Domain.Apps.Entities.Contents.Counter
{ {
public sealed class CounterJintExtension : IJintExtension public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor
{ {
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
@ -39,6 +40,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
} }
} }
public void Describe(AddDescription describe, ScriptScope scope)
{
describe(JsonType.Function, "incrementCounter(name)",
Resources.ScriptingIncrementCounter);
describe(JsonType.Function, "resetCounter(name, value)",
Resources.ScriptingResetCounter);
}
private long Increment(DomainId appId, string name) private long Increment(DomainId appId, string name)
{ {
var grain = grainFactory.GetGrain<ICounterGrain>(appId.ToString()); var grain = grainFactory.GetGrain<ICounterGrain>(appId.ToString());

112
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -10,8 +10,7 @@ using Microsoft.Extensions.Options;
using Microsoft.OData; using Microsoft.OData;
using Microsoft.OData.Edm; using Microsoft.OData.Edm;
using NJsonSchema; using NJsonSchema;
using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.GenerateFilters;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text;
@ -24,15 +23,12 @@ using Squidex.Infrastructure.Queries.Json;
using Squidex.Infrastructure.Queries.OData; using Squidex.Infrastructure.Queries.OData;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
using Squidex.Text;
namespace Squidex.Domain.Apps.Entities.Contents.Queries namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public class ContentQueryParser public class ContentQueryParser
{ {
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60);
private readonly EdmModel genericEdmModel = BuildEdmModel("Generic", "Content", new EdmModel(), null);
private readonly JsonSchema genericJsonSchema = ContentJsonSchemaBuilder.BuildSchema(null, false, true);
private readonly IMemoryCache cache; private readonly IMemoryCache cache;
private readonly IJsonSerializer jsonSerializer; private readonly IJsonSerializer jsonSerializer;
private readonly IAppProvider appprovider; private readonly IAppProvider appprovider;
@ -176,17 +172,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private ClrQuery ParseJson(Context context, ISchemaEntity? schema, Query<IJsonValue> query, private ClrQuery ParseJson(Context context, ISchemaEntity? schema, Query<IJsonValue> query,
ResolvedComponents components) ResolvedComponents components)
{ {
var jsonSchema = BuildJsonSchema(context, schema, components); var queryModel = BuildQueryModel(context, schema, components);
return jsonSchema.Convert(query); return queryModel.Convert(query);
} }
private ClrQuery ParseJson(Context context, ISchemaEntity? schema, string json, private ClrQuery ParseJson(Context context, ISchemaEntity? schema, string json,
ResolvedComponents components) ResolvedComponents components)
{ {
var jsonSchema = BuildJsonSchema(context, schema, components); var queryModel = BuildQueryModel(context, schema, components);
return jsonSchema.Parse(json, jsonSerializer); return queryModel.Parse(json, jsonSerializer);
} }
private ClrQuery ParseOData(Context context, ISchemaEntity? schema, string odata, private ClrQuery ParseOData(Context context, ISchemaEntity? schema, string odata,
@ -218,129 +214,53 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
} }
private JsonSchema BuildJsonSchema(Context context, ISchemaEntity? schema, private QueryModel BuildQueryModel(Context context, ISchemaEntity? schema,
ResolvedComponents components) ResolvedComponents components)
{ {
if (schema == null)
{
return genericJsonSchema;
}
var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient); var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient);
var result = cache.GetOrCreate(cacheKey, entry => var result = cache.GetOrCreate(cacheKey, entry =>
{ {
entry.AbsoluteExpirationRelativeToNow = CacheTime; entry.AbsoluteExpirationRelativeToNow = CacheTime;
return BuildJsonSchema(schema.SchemaDef, context.App, components, context.IsFrontendClient); return ContentQueryModel.Build(schema?.SchemaDef, context.App.PartitionResolver(), components);
}); });
return result; return result;
} }
private static JsonSchema BuildJsonSchema(Schema schema, IAppEntity app,
ResolvedComponents components, bool withHiddenFields)
{
var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), components, null, withHiddenFields);
return ContentJsonSchemaBuilder.BuildSchema(dataSchema, false, true);
}
private IEdmModel BuildEdmModel(Context context, ISchemaEntity? schema, private IEdmModel BuildEdmModel(Context context, ISchemaEntity? schema,
ResolvedComponents components) ResolvedComponents components)
{ {
if (schema == null)
{
return genericEdmModel;
}
var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient); var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient);
var result = cache.GetOrCreate<IEdmModel>(cacheKey, entry => var result = cache.GetOrCreate<IEdmModel>(cacheKey, entry =>
{ {
entry.AbsoluteExpirationRelativeToNow = CacheTime; entry.AbsoluteExpirationRelativeToNow = CacheTime;
return BuildEdmModel(schema.SchemaDef, context.App, components, context.IsFrontendClient); return BuildQueryModel(context, schema, components).ConvertToEdm("Contents", schema?.SchemaDef.Name ?? "Generic");
}); });
return result; return result;
} }
private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, private static string BuildEmdCacheKey(IAppEntity app, ISchemaEntity? schema, bool withHidden)
ResolvedComponents components, bool withHiddenFields)
{ {
var model = new EdmModel(); if (schema == null)
var pascalAppName = app.Name.ToPascalCase();
var pascalSchemaName = schema.Name.ToPascalCase();
var typeFactory = new EdmTypeFactory(name =>
{ {
var finalName = pascalSchemaName; return $"EDM/__generic";
}
if (!string.IsNullOrWhiteSpace(name))
{
finalName += ".";
finalName += name;
}
var result = model.SchemaElements.OfType<EdmComplexType>().FirstOrDefault(x => x.Name == finalName);
if (result != null)
{
return (result, false);
}
result = new EdmComplexType(pascalAppName, finalName);
model.AddElement(result);
return (result, true);
});
var schemaType = schema.BuildEdmType(withHiddenFields, app.PartitionResolver(), typeFactory, components);
return BuildEdmModel(app.Name.ToPascalCase(), schema.Name, model, schemaType); return $"EDM/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}";
} }
private static EdmModel BuildEdmModel(string modelName, string name, EdmModel model, EdmComplexType? schemaType) private static string BuildJsonCacheKey(IAppEntity app, ISchemaEntity? schema, bool withHidden)
{ {
var entityType = new EdmEntityType(modelName, name); if (schema == null)
entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("isDeleted", EdmPrimitiveTypeKind.Boolean);
entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty("createdBy", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty("lastModifiedBy", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("newStatus", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("status", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("version", EdmPrimitiveTypeKind.Int32);
if (schemaType != null)
{ {
entityType.AddStructuralProperty("data", new EdmComplexTypeReference(schemaType, false)); return $"JSON/__generic";
model.AddElement(schemaType);
} }
var container = new EdmEntityContainer("Squidex", "Container");
container.AddEntitySet("ContentSet", entityType);
model.AddElement(container);
model.AddElement(entityType);
return model;
}
private static string BuildEmdCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden)
{
return $"EDM/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}";
}
private static string BuildJsonCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden)
{
return $"JSON/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; return $"JSON/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}";
} }
} }

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

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public static readonly GeoQueryTransformer Instance = new GeoQueryTransformer(); public static readonly GeoQueryTransformer Instance = new GeoQueryTransformer();
public sealed record Args(Context Context, ISchemaEntity Schema, ITextIndex TextIndex); public record struct Args(Context Context, ISchemaEntity Schema, ITextIndex TextIndex);
private GeoQueryTransformer() private GeoQueryTransformer()
{ {

12
backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs

@ -12,12 +12,13 @@ using Jint.Runtime;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Properties;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public sealed class ReferencesJintExtension : IJintExtension public sealed class ReferencesJintExtension : IJintExtension, IScriptDescriptor
{ {
private delegate void GetReferencesDelegate(JsValue references, Action<JsValue> callback); private delegate void GetReferencesDelegate(JsValue references, Action<JsValue> callback);
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
@ -45,6 +46,15 @@ namespace Squidex.Domain.Apps.Entities.Contents
context.Engine.SetValue("getReferences", action); context.Engine.SetValue("getReferences", action);
} }
public void Describe(AddDescription describe, ScriptScope scope)
{
describe(JsonType.Function, "getReferences(ids, callback)",
Resources.ScriptingGetReferences);
describe(JsonType.Function, "getReference(ids, callback)",
Resources.ScriptingGetReference);
}
private void GetReferences(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback) private void GetReferences(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{ {
GetReferencesAsync(context, appId, user, references, callback).Forget(); GetReferencesAsync(context, appId, user, references, callback).Forget();

126
backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs

@ -0,0 +1,126 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Squidex.Domain.Apps.Entities.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Entities.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Queries the asset with the specified ID and invokes the callback with an array of assets..
/// </summary>
internal static string ScriptingGetAsset {
get {
return ResourceManager.GetString("ScriptingGetAsset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to ueries the assets with the specified IDs and invokes the callback with an array of assets..
/// </summary>
internal static string ScriptingGetAssets {
get {
return ResourceManager.GetString("ScriptingGetAssets", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Get the text of an asset. Encodings: base64,ascii,unicode,utf8.
/// </summary>
internal static string ScriptingGetAssetText {
get {
return ResourceManager.GetString("ScriptingGetAssetText", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Queries the content item with the specified ID and invokes the callback with an array of contents..
/// </summary>
internal static string ScriptingGetReference {
get {
return ResourceManager.GetString("ScriptingGetReference", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Queries the content items with the specified IDs and invokes the callback with an array of contents..
/// </summary>
internal static string ScriptingGetReferences {
get {
return ResourceManager.GetString("ScriptingGetReferences", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Increments the counter with the given name and returns the value..
/// </summary>
internal static string ScriptingIncrementCounter {
get {
return ResourceManager.GetString("ScriptingIncrementCounter", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Resets the counter with the given name to zero..
/// </summary>
internal static string ScriptingResetCounter {
get {
return ResourceManager.GetString("ScriptingResetCounter", resourceCulture);
}
}
}
}

141
backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ScriptingGetAsset" xml:space="preserve">
<value>Queries the asset with the specified ID and invokes the callback with an array of assets.</value>
</data>
<data name="ScriptingGetAssets" xml:space="preserve">
<value>ueries the assets with the specified IDs and invokes the callback with an array of assets.</value>
</data>
<data name="ScriptingGetAssetText" xml:space="preserve">
<value>Get the text of an asset. Encodings: base64,ascii,unicode,utf8</value>
</data>
<data name="ScriptingGetReference" xml:space="preserve">
<value>Queries the content item with the specified ID and invokes the callback with an array of contents.</value>
</data>
<data name="ScriptingGetReferences" xml:space="preserve">
<value>Queries the content items with the specified IDs and invokes the callback with an array of contents.</value>
</data>
<data name="ScriptingIncrementCounter" xml:space="preserve">
<value>Increments the counter with the given name and returns the value.</value>
</data>
<data name="ScriptingResetCounter" xml:space="preserve">
<value>Resets the counter with the given name to zero.</value>
</data>
</root>

313
backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptingCompletion.cs

@ -1,313 +0,0 @@
// ==========================================================================
// 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.Schemas;
namespace Squidex.Domain.Apps.Entities.Scripting
{
public sealed class ScriptingCompletion
{
private readonly Stack<string> prefixes = new Stack<string>();
private readonly HashSet<ScriptingValue> result = new HashSet<ScriptingValue>();
public IReadOnlyList<ScriptingValue> Content(Schema schema, PartitionResolver partitionResolver)
{
AddFunction("replace()",
"Tell Squidex that you have modified the data and that the change should be applied.");
AddFunction("getReferences(ids, callback)",
"Queries the content items with the specified IDs and invokes the callback with an array of contents.");
AddFunction("getReference(ids, callback)",
"Queries the content item with the specified ID and invokes the callback with an array of contents.");
AddFunction("getAssets(ids, callback)",
"Queries the assets with the specified IDs and invokes the callback with an array of assets.");
AddFunction("getAsset(ids, callback)",
"Queries the asset with the specified ID and invokes the callback with an array of assets.");
AddShared();
AddObject("ctx", FieldDescriptions.Context, () =>
{
AddString("contentId",
FieldDescriptions.EntityId);
AddString("status",
FieldDescriptions.ContentStatus);
AddString("statusOld",
FieldDescriptions.ContentStatusOld);
AddObject("data", FieldDescriptions.ContentData, () =>
{
AddData(schema, partitionResolver);
});
AddObject("dataOld", FieldDescriptions.ContentDataOld, () =>
{
AddData(schema, partitionResolver);
});
});
return result.OrderBy(x => x.Path).ToList();
}
public IReadOnlyList<ScriptingValue> Asset()
{
AddShared();
AddObject("ctx", FieldDescriptions.Context, () =>
{
AddString("assetId",
FieldDescriptions.EntityId);
AddObject("asset",
FieldDescriptions.Asset, () =>
{
AddSharedAsset();
AddNumber("fileVersion",
FieldDescriptions.AssetFileVersion);
});
AddObject("command",
FieldDescriptions.Command, () =>
{
AddSharedAsset();
AddBoolean("permanent",
FieldDescriptions.EntityRequestDeletePermanent);
});
});
return result.OrderBy(x => x.Path).ToList();
}
private void AddSharedAsset()
{
AddString("fileHash",
FieldDescriptions.AssetFileHash);
AddString("fileName",
FieldDescriptions.AssetFileName);
AddString("fileSize",
FieldDescriptions.AssetFileSize);
AddString("fileSlug",
FieldDescriptions.AssetSlug);
AddString("mimeType",
FieldDescriptions.AssetMimeType);
AddBoolean("isProtected",
FieldDescriptions.AssetIsProtected);
AddString("parentId",
FieldDescriptions.AssetParentId);
AddArray("parentPath",
FieldDescriptions.AssetParentPath);
AddArray("tags",
FieldDescriptions.AssetTags);
AddObject("metadata",
FieldDescriptions.AssetMetadata, () =>
{
AddArray("name",
FieldDescriptions.AssetMetadataValue);
});
}
private void AddShared()
{
AddFunction("disallow()",
"Tell Squidex to not allow the current operation and to return a 403 (Forbidden).");
AddFunction("reject('Reason')",
"Tell Squidex to reject the current operation and to return a 403 (Forbidden).");
AddFunction("html2Text(text)",
"Converts a HTML string to plain text.");
AddFunction("markdown2Text(text)",
"Converts a markdown string to plain text.");
AddFunction("formatDate(data, pattern)",
"Formats a JavaScript date object using the specified pattern.");
AddFunction("formatTime(text)",
"Formats a JavaScript date object using the specified pattern.");
AddFunction("wordCount(text)",
"Counts the number of words in a text. Useful in combination with html2Text or markdown2Text.");
AddFunction("characterCount(text)",
"Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text.");
AddFunction("toCamelCase(text)",
"Converts a text to camelCase.");
AddFunction("toPascalCase(text)",
"Calculate the SHA256 hash from a given string. Use this method for hashing passwords");
AddFunction("sha256(text)",
"Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important.");
AddFunction("slugify(text)",
"Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.");
AddFunction("slugify(text)",
"Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.");
AddFunction("slugify(text)",
"Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.");
AddFunction("slugify(text)",
"Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.");
AddFunction("getJSON(url, callback, ?headers)",
"Makes a GET request to the defined URL and parses the result as JSON. Headers are optional.");
AddFunction("postJSON(url, body, callback, ?headers)",
"Makes a POST request to the defined URL and parses the result as JSON. Headers are optional.");
AddFunction("putJSON(url, body, callback, ?headers)",
"Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional.");
AddFunction("putJSON(url, body, callback, ?headers)",
"Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional.");
AddFunction("patchJSON(url, body, callback, headers)",
"Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional.");
AddFunction("deleteJSON(url, body, callback, headers)",
"Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional.");
AddString("appId",
FieldDescriptions.AppId);
AddString("appName",
FieldDescriptions.AppName);
AddString("operation",
FieldDescriptions.Operation);
AddObject("user",
FieldDescriptions.User, () =>
{
AddString("id",
FieldDescriptions.UserId);
AddString("email",
FieldDescriptions.UserEmail);
AddBoolean("isClient",
FieldDescriptions.UserIsClient);
AddBoolean("isUser",
FieldDescriptions.UserIsUser);
AddObject("claims",
FieldDescriptions.UserClaims, () =>
{
AddArray("name",
FieldDescriptions.UsersClaimsValue);
});
});
}
private void AddData(Schema schema, PartitionResolver partitionResolver)
{
foreach (var field in schema.Fields.Where(x => x.IsForApi(true)))
{
var description = $"The values of the '{field.DisplayName()}' field.";
AddObject(field.Name, $"The values of the '{field.DisplayName()}' field.", () =>
{
foreach (var partition in partitionResolver(field.Partitioning).AllKeys)
{
var description = $"The '{partition}' value of the '{field.DisplayName()}' field.";
if (field is ArrayField arrayField)
{
AddObject(partition, description, () =>
{
foreach (var nestedField in arrayField.Fields.Where(x => x.IsForApi(true)))
{
var description = $"The value of the '{nestedField.DisplayName()}' nested field.";
AddAny(field.Name, description);
}
});
}
else
{
AddAny(partition, description);
}
}
});
}
}
private void AddAny(string name, string description)
{
Add(JsonType.Any, name, description);
}
private void AddArray(string name, string description)
{
Add(JsonType.Array, name, description);
}
private void AddBoolean(string name, string description)
{
Add(JsonType.Boolean, name, description);
}
private void AddFunction(string name, string description)
{
Add(JsonType.Function, name, description);
}
private void AddNumber(string name, string description)
{
Add(JsonType.Number, name, description);
}
private void AddString(string name, string description)
{
Add(JsonType.String, name, description);
}
private void Add(JsonType type, string name, string description)
{
var fullName = string.Join('.', prefixes.Reverse().Union(Enumerable.Repeat(name, 1)));
result.Add(new ScriptingValue(fullName, type, description));
}
private void AddObject(string name, string description, Action inner)
{
Add(JsonType.Object, description, name);
prefixes.Push(name);
try
{
inner();
}
finally
{
prefixes.Pop();
}
}
}
}

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

@ -41,4 +41,17 @@
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> <AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project> </Project>

4
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.MongoDb.Queries
{ {
if (!supportsSearch) if (!supportsSearch)
{ {
throw new ValidationException(T.Get("common.fullTextNotSupported")); throw new ValidationException(T.Get("queries.fullTextNotSupported"));
} }
return (Builders<TDocument>.Filter.Text(query.FullText), false); return (Builders<TDocument>.Filter.Text(query.FullText), false);

5
backend/src/Squidex.Infrastructure/CollectionExtensions.cs

@ -85,11 +85,6 @@ namespace Squidex.Infrastructure
return false; return false;
} }
public static IEnumerable<TElement> SingleGroups<TKey, TElement>(this IEnumerable<IGrouping<TKey, TElement>> source)
{
return source.Where(x => x.Count() == 1).Select(x => x.First());
}
public static bool SetEquals<T>(this IReadOnlyCollection<T> source, IReadOnlyCollection<T> other) public static bool SetEquals<T>(this IReadOnlyCollection<T> source, IReadOnlyCollection<T> other)
{ {
return source.Count == other.Count && source.Intersect(other).Count() == other.Count; return source.Count == other.Count && source.Intersect(other).Count() == other.Count;

162
backend/src/Squidex.Infrastructure/Properties/Resources.Designer.cs

@ -0,0 +1,162 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Squidex.Infrastructure.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Infrastructure.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Json query not valid: {0}..
/// </summary>
internal static string QueryInvalid {
get {
return ResourceManager.GetString("QueryInvalid", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Array value is not allowed for &apos;{0}&apos; operator and path &apos;{1}&apos;..
/// </summary>
internal static string QueryInvalidArray {
get {
return ResourceManager.GetString("QueryInvalidArray", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Json query not valid json: {0}..
/// </summary>
internal static string QueryInvalidJson {
get {
return ResourceManager.GetString("QueryInvalidJson", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Either &apos;and&apos;, &apos;or&apos;, &apos;not&apos; or &apos;path&apos;+&apos;op&apos; must be set..
/// </summary>
internal static string QueryInvalidJsonStructure {
get {
return ResourceManager.GetString("QueryInvalidJsonStructure", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &apos;{0}&apos; is not a valid operator for type {1} at &apos;{2}&apos;..
/// </summary>
internal static string QueryInvalidOperator {
get {
return ResourceManager.GetString("QueryInvalidOperator", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Path &apos;{0}&apos; does not point to a valid property in the model..
/// </summary>
internal static string QueryInvalidPath {
get {
return ResourceManager.GetString("QueryInvalidPath", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} is not a valid regular expression at path &apos;{1}&apos;..
/// </summary>
internal static string QueryInvalidRegex {
get {
return ResourceManager.GetString("QueryInvalidRegex", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Expected {0} for path &apos;{2}&apos;, but got {1}..
/// </summary>
internal static string QueryWrongExpectedType {
get {
return ResourceManager.GetString("QueryWrongExpectedType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Expected {0} for path &apos;{1}&apos;, but got invalid String..
/// </summary>
internal static string QueryWrongFormat {
get {
return ResourceManager.GetString("QueryWrongFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Expected primitive for path &apos;{1}&apos;, but got {0}..
/// </summary>
internal static string QueryWrongPrimitive {
get {
return ResourceManager.GetString("QueryWrongPrimitive", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unsupported type {0} for path &apos;{1}&apos;..
/// </summary>
internal static string QueryWrongType {
get {
return ResourceManager.GetString("QueryWrongType", resourceCulture);
}
}
}
}

153
backend/src/Squidex.Infrastructure/Properties/Resources.resx

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="QueryInvalid" xml:space="preserve">
<value>Json query not valid: {0}.</value>
</data>
<data name="QueryInvalidArray" xml:space="preserve">
<value>Array value is not allowed for '{0}' operator and path '{1}'.</value>
</data>
<data name="QueryInvalidJson" xml:space="preserve">
<value>Json query not valid json: {0}.</value>
</data>
<data name="QueryInvalidJsonStructure" xml:space="preserve">
<value>Either 'and', 'or', 'not' or 'path'+'op' must be set.</value>
</data>
<data name="QueryInvalidOperator" xml:space="preserve">
<value>'{0}' is not a valid operator for type {1} at '{2}'.</value>
</data>
<data name="QueryInvalidPath" xml:space="preserve">
<value>Path '{0}' does not point to a valid property in the model.</value>
</data>
<data name="QueryInvalidRegex" xml:space="preserve">
<value>{0} is not a valid regular expression at path '{1}'.</value>
</data>
<data name="QueryWrongExpectedType" xml:space="preserve">
<value>Expected {0} for path '{2}', but got {1}.</value>
</data>
<data name="QueryWrongFormat" xml:space="preserve">
<value>Expected {0} for path '{1}', but got invalid String.</value>
</data>
<data name="QueryWrongPrimitive" xml:space="preserve">
<value>Expected primitive for path '{1}', but got {0}.</value>
</data>
<data name="QueryWrongType" xml:space="preserve">
<value>Unsupported type {0} for path '{1}'.</value>
</data>
</root>

3
backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs

@ -5,8 +5,11 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.ComponentModel;
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
[TypeConverter(typeof(CompareOperatorTypeConverter))]
public enum CompareOperator public enum CompareOperator
{ {
Contains, Contains,

99
backend/src/Squidex.Infrastructure/Queries/CompareOperatorTypeConverter.cs

@ -0,0 +1,99 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel;
using System.Globalization;
namespace Squidex.Infrastructure.Queries
{
public sealed class CompareOperatorTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
var op = (string)value;
switch (op.ToLowerInvariant())
{
case "eq":
return CompareOperator.Equals;
case "ne":
return CompareOperator.NotEquals;
case "lt":
return CompareOperator.LessThan;
case "le":
return CompareOperator.LessThanOrEqual;
case "gt":
return CompareOperator.GreaterThan;
case "ge":
return CompareOperator.GreaterThanOrEqual;
case "empty":
return CompareOperator.Empty;
case "exists":
return CompareOperator.Exists;
case "matchs":
return CompareOperator.Matchs;
case "contains":
return CompareOperator.Contains;
case "endswith":
return CompareOperator.EndsWith;
case "startswith":
return CompareOperator.StartsWith;
case "in":
return CompareOperator.In;
}
throw new InvalidCastException($"Unexpected compare operator, got {op}.");
}
public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
{
var op = (CompareOperator)value!;
switch (op)
{
case CompareOperator.Equals:
return "eq";
case CompareOperator.NotEquals:
return "ne";
case CompareOperator.LessThan:
return "lt";
case CompareOperator.LessThanOrEqual:
return "le";
case CompareOperator.GreaterThan:
return "gt";
case CompareOperator.GreaterThanOrEqual:
return "gt";
case CompareOperator.Empty:
return "empty";
case CompareOperator.Exists:
return "exists";
case CompareOperator.Matchs:
return "matchs";
case CompareOperator.Contains:
return "contains";
case CompareOperator.EndsWith:
return "endsWith";
case CompareOperator.StartsWith:
return "startsWith";
case CompareOperator.In:
return "in";
}
throw new InvalidCastException($"Unexpected compare operator, got {op}.");
}
}
}

14
backend/src/Squidex.Infrastructure/Queries/FilterField.cs

@ -0,0 +1,14 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Infrastructure.Queries
{
public sealed record FilterField(FilterSchema Schema, string Path, string? Description = null,
bool IsNullable = false);
}

104
backend/src/Squidex.Infrastructure/Queries/FilterSchema.cs

@ -0,0 +1,104 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Collections;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Infrastructure.Queries
{
public sealed record FilterSchema(FilterSchemaType Type)
{
public static readonly FilterSchema Any = new FilterSchema(FilterSchemaType.Any);
public static readonly FilterSchema Boolean = new FilterSchema(FilterSchemaType.Boolean);
public static readonly FilterSchema Date = new FilterSchema(FilterSchemaType.Date);
public static readonly FilterSchema DateTime = new FilterSchema(FilterSchemaType.DateTime);
public static readonly FilterSchema GeoObject = new FilterSchema(FilterSchemaType.GeoObject);
public static readonly FilterSchema Guid = new FilterSchema(FilterSchemaType.Guid);
public static readonly FilterSchema Number = new FilterSchema(FilterSchemaType.Number);
public static readonly FilterSchema String = new FilterSchema(FilterSchemaType.String);
public static readonly FilterSchema StringArray = new FilterSchema(FilterSchemaType.StringArray);
public ReadonlyList<FilterField>? Fields { get; init; }
public object? Extra { get; init; }
public FilterSchema Flatten(int maxDepth = 7, Predicate<FilterSchema>? predicate = null)
{
if (Fields == null || Fields.Count == 0)
{
return this;
}
var result = new List<FilterField>();
var pathStack = new Stack<string>();
void AddField(FilterField field)
{
pathStack.Push(field.Path);
if (predicate?.Invoke(field.Schema) != false)
{
var path = string.Join('.', pathStack.Reverse());
var schema = field.Schema;
if (schema.Fields != null)
{
schema = schema with { Fields = null };
}
result?.Add(field with { Path = path, Schema = schema });
}
if (field.Schema.Fields?.Count > 0 && pathStack.Count < maxDepth)
{
AddFields(field.Schema.Fields);
}
pathStack.Pop();
}
void AddFields(IEnumerable<FilterField> source)
{
foreach (var field in source)
{
AddField(field);
}
}
AddFields(Fields);
var conflictFree = GetConflictFreeFields(result);
return this with
{
Fields = conflictFree.ToReadonlyList()
};
}
public static IEnumerable<FilterField> GetConflictFreeFields(IEnumerable<FilterField> fields)
{
var conflictFree = fields.GroupBy(x => x.Path).Select(group =>
{
var firstType = group.First().Schema.Type;
if (group.All(x => x.Schema.Type == firstType))
{
return group.Take(1);
}
else
{
return Enumerable.Empty<FilterField>();
}
}).SelectMany(x => x);
return conflictFree;
}
}
}

24
backend/src/Squidex.Infrastructure/Queries/FilterSchemaType.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public enum FilterSchemaType
{
Any,
Boolean,
Date,
DateTime,
GeoObject,
Guid,
Number,
Object,
ObjectArray,
String,
StringArray
}
}

46
backend/src/Squidex.Infrastructure/Queries/Json/CompareOperatorJsonConverter.cs

@ -0,0 +1,46 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json;
using JsonException = Squidex.Infrastructure.Json.JsonException;
namespace Squidex.Infrastructure.Queries.Json
{
public sealed class CompareOperatorJsonConverter : JsonConverter, ISupportedTypes
{
private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(CompareOperator));
public IEnumerable<Type> SupportedTypes
{
get { yield return typeof(CompareOperator); }
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
try
{
return typeConverter.ConvertFromInvariantString(reader.Value?.ToString()!);
}
catch (InvalidCastException ex)
{
throw new JsonException(ex.Message);
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
writer.WriteValue(typeConverter.ConvertToInvariantString(value));
}
public override bool CanConvert(Type objectType)
{
return SupportedTypes.Contains(objectType);
}
}
}

70
backend/src/Squidex.Infrastructure/Queries/Json/Errors.cs

@ -0,0 +1,70 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Globalization;
using Squidex.Infrastructure.Properties;
namespace Squidex.Infrastructure.Queries.Json
{
internal static class Errors
{
public static string InvalidJsonStructure()
{
return Resources.QueryInvalidJsonStructure;
}
public static string InvalidQuery(object message)
{
return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalid, message);
}
public static string InvalidQueryJson(object message)
{
return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidJson, message);
}
public static string InvalidPath(PropertyPath path)
{
return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidPath, path);
}
public static string InvalidOperator(object @operator, object type, PropertyPath path)
{
return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidOperator, @operator, type, path);
}
public static string InvalidRegex(object value, PropertyPath path)
{
return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidRegex, value, path);
}
public static string InvalidArray(object @operator, PropertyPath path)
{
return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidArray, @operator, path);
}
public static string WrongExpectedType(object expected, object type, PropertyPath path)
{
return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongExpectedType, expected, type, path);
}
public static string WrongFormat(object expected, PropertyPath path)
{
return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongFormat, expected, path);
}
public static string WrongType(object type, PropertyPath path)
{
return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongType, type, path);
}
public static string WrongPrimitive(object type, PropertyPath path)
{
return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongPrimitive, type, path);
}
}
}

47
backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs

@ -8,7 +8,7 @@
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries.Json
{ {
public sealed class JsonFilterSurrogate : ISurrogate<FilterNode<IJsonValue>> public sealed class JsonFilterSurrogate : ISurrogate<FilterNode<IJsonValue>>
{ {
@ -18,7 +18,7 @@ namespace Squidex.Infrastructure.Queries
public FilterNode<IJsonValue>? Not { get; set; } public FilterNode<IJsonValue>? Not { get; set; }
public string? Op { get; set; } public CompareOperator? Op { get; set; }
public string? Path { get; set; } public string? Path { get; set; }
@ -46,49 +46,12 @@ namespace Squidex.Infrastructure.Queries
return new LogicalFilter<IJsonValue>(LogicalFilterType.Or, Or); return new LogicalFilter<IJsonValue>(LogicalFilterType.Or, Or);
} }
if (!string.IsNullOrWhiteSpace(Path) && !string.IsNullOrWhiteSpace(Op)) if (!string.IsNullOrWhiteSpace(Path) && Op != null)
{ {
var @operator = ReadOperator(Op); return new CompareFilter<IJsonValue>(Path, Op.Value, Value ?? JsonValue.Null);
return new CompareFilter<IJsonValue>(Path, @operator, Value ?? JsonValue.Null);
}
throw new JsonException("Invalid query.");
}
private static CompareOperator ReadOperator(string op)
{
switch (op.ToLowerInvariant())
{
case "eq":
return CompareOperator.Equals;
case "ne":
return CompareOperator.NotEquals;
case "lt":
return CompareOperator.LessThan;
case "le":
return CompareOperator.LessThanOrEqual;
case "gt":
return CompareOperator.GreaterThan;
case "ge":
return CompareOperator.GreaterThanOrEqual;
case "empty":
return CompareOperator.Empty;
case "exists":
return CompareOperator.Exists;
case "matchs":
return CompareOperator.Matchs;
case "contains":
return CompareOperator.Contains;
case "endswith":
return CompareOperator.EndsWith;
case "startswith":
return CompareOperator.StartsWith;
case "in":
return CompareOperator.In;
} }
throw new JsonException($"Unexpected compare operator, got {op}."); throw new JsonException(Errors.InvalidJsonStructure());
} }
} }
} }

44
backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using NJsonSchema;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
@ -17,15 +16,15 @@ namespace Squidex.Infrastructure.Queries.Json
{ {
private static readonly JsonFilterVisitor Instance = new JsonFilterVisitor(); private static readonly JsonFilterVisitor Instance = new JsonFilterVisitor();
public sealed record Args(JsonSchema Schema, List<string> Errors); public record struct Args(QueryModel Model, List<string> Errors);
private JsonFilterVisitor() private JsonFilterVisitor()
{ {
} }
public static FilterNode<ClrValue>? Parse(FilterNode<IJsonValue> filter, JsonSchema schema, List<string> errors) public static FilterNode<ClrValue>? Parse(FilterNode<IJsonValue> filter, QueryModel model, List<string> errors)
{ {
var args = new Args(schema, errors); var args = new Args(model, errors);
var parsed = filter.Accept(Instance, args); var parsed = filter.Accept(Instance, args);
@ -51,25 +50,21 @@ namespace Squidex.Infrastructure.Queries.Json
public override FilterNode<ClrValue> Visit(CompareFilter<IJsonValue> nodeIn, Args args) public override FilterNode<ClrValue> Visit(CompareFilter<IJsonValue> nodeIn, Args args)
{ {
CompareFilter<ClrValue>? result = null; var fieldMatches = nodeIn.Path.GetMatchingFields(args.Model.Schema, args.Errors);
var fieldErrors = new List<string>();
if (nodeIn.Path.TryGetProperty(args.Schema, args.Errors, out var property)) foreach (var field in fieldMatches)
{ {
var isValidOperator = OperatorValidator.IsAllowedOperator(property, nodeIn.Operator); fieldErrors.Clear();
var isValidOperator = args.Model.Operators.TryGetValue(field.Schema.Type, out var operators) && operators.Contains(nodeIn.Operator);
if (!isValidOperator) if (!isValidOperator)
{ {
var name = property.Type.ToString(); fieldErrors.Add(Errors.InvalidOperator(nodeIn.Operator, field.Schema.Type, nodeIn.Path));
if (!string.IsNullOrWhiteSpace(property.Format))
{
name = $"{name}({property.Format})";
}
args.Errors.Add($"'{nodeIn.Operator}' is not a valid operator for type {name} at '{nodeIn.Path}'.");
} }
var value = ValueConverter.Convert(property, nodeIn.Value, nodeIn.Path, args.Errors); var value = ValueConverter.Convert(field, nodeIn.Value, nodeIn.Path, fieldErrors);
if (value != null && isValidOperator) if (value != null && isValidOperator)
{ {
@ -84,22 +79,27 @@ namespace Squidex.Infrastructure.Queries.Json
{ {
if (value.IsList) if (value.IsList)
{ {
args.Errors.Add($"Array value is not allowed for '{nodeIn.Operator}' operator and path '{nodeIn.Path}'."); fieldErrors.Add(Errors.InvalidArray(nodeIn.Operator, nodeIn.Path));
} }
} }
if (nodeIn.Operator == CompareOperator.Matchs && value.Value?.ToString()?.IsValidRegex() != true) if (nodeIn.Operator == CompareOperator.Matchs && value.Value?.ToString()?.IsValidRegex() != true)
{ {
args.Errors.Add($"{value} is not a valid regular expression."); fieldErrors.Add(Errors.InvalidRegex(value.ToString(), nodeIn.Path));
} }
}
result = new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, value); if (args.Errors.Count == 0 && fieldErrors.Count == 0 && value != null)
{
return new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, value);
}
else if (field == fieldMatches.Last())
{
args.Errors.AddRange(fieldErrors);
} }
} }
result ??= new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, ClrValue.Null); return new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, ClrValue.Null);
return result;
} }
} }
} }

86
backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs

@ -1,86 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema;
using Squidex.Infrastructure.Json;
namespace Squidex.Infrastructure.Queries.Json
{
public static class OperatorValidator
{
private static readonly CompareOperator[] BooleanOperators =
{
CompareOperator.Equals,
CompareOperator.Exists,
CompareOperator.In,
CompareOperator.NotEquals
};
private static readonly CompareOperator[] NumberOperators =
{
CompareOperator.Equals,
CompareOperator.Exists,
CompareOperator.LessThan,
CompareOperator.LessThanOrEqual,
CompareOperator.GreaterThan,
CompareOperator.GreaterThanOrEqual,
CompareOperator.In,
CompareOperator.NotEquals
};
private static readonly CompareOperator[] StringOperators =
{
CompareOperator.Contains,
CompareOperator.Empty,
CompareOperator.Exists,
CompareOperator.EndsWith,
CompareOperator.Equals,
CompareOperator.GreaterThan,
CompareOperator.GreaterThanOrEqual,
CompareOperator.In,
CompareOperator.LessThan,
CompareOperator.LessThanOrEqual,
CompareOperator.Matchs,
CompareOperator.NotEquals,
CompareOperator.StartsWith
};
private static readonly CompareOperator[] ArrayOperators =
{
CompareOperator.Empty,
CompareOperator.Exists,
CompareOperator.Equals,
CompareOperator.In,
CompareOperator.NotEquals
};
private static readonly CompareOperator[] GeoOperators =
{
CompareOperator.LessThan,
CompareOperator.Exists
};
public static bool IsAllowedOperator(JsonSchema schema, CompareOperator compareOperator)
{
switch (schema.Type)
{
case JsonObjectType.None:
return true;
case JsonObjectType.Boolean:
return BooleanOperators.Contains(compareOperator);
case JsonObjectType.Integer:
return NumberOperators.Contains(compareOperator);
case JsonObjectType.Number:
return NumberOperators.Contains(compareOperator);
case JsonObjectType.String:
return StringOperators.Contains(compareOperator);
case JsonObjectType.Array:
return ArrayOperators.Contains(compareOperator);
case JsonObjectType.Object when schema.Format == GeoJson.Format:
return GeoOperators.Contains(compareOperator);
}
return false;
}
}
}

47
backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs

@ -5,48 +5,47 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Diagnostics.CodeAnalysis;
using NJsonSchema;
namespace Squidex.Infrastructure.Queries.Json namespace Squidex.Infrastructure.Queries.Json
{ {
public static class PropertyPathValidator public static class PropertyPathValidator
{ {
public static bool TryGetProperty(this PropertyPath path, JsonSchema schema, List<string> errors, [MaybeNullWhen(false)] out JsonSchema property) public static IEnumerable<FilterField> GetMatchingFields(this PropertyPath path, FilterSchema schema, List<string> errors)
{ {
foreach (var element in path) var lastIndex = path.Count - 1;
{
var parent = schema.Reference ?? schema;
if (parent.Properties.TryGetValue(element, out var p)) List<FilterField>? result = null;
{
schema = p;
if (schema.Type == JsonObjectType.None && schema.Reference == null) void Check(int index, FilterSchema schema)
{ {
break; if (schema.Fields == null)
} {
return;
} }
else
var fields = schema.Fields.Where(x => x.Path == path[index]);
foreach (var field in fields)
{ {
if (!string.IsNullOrWhiteSpace(parent.Title)) if (index == lastIndex || field.Schema.Type == FilterSchemaType.Any)
{ {
errors.Add($"'{element}' is not a property of '{parent.Title}'."); result ??= new List<FilterField>();
result.Add(field);
} }
else else
{ {
errors.Add($"Path '{path}' does not point to a valid property in the model."); Check(index + 1, field.Schema);
} }
property = null!;
return false;
} }
} }
property = schema; Check(0, schema);
if (result == null)
{
errors.Add(Errors.InvalidPath(path.ToString()));
}
return true; return result as IEnumerable<FilterField> ?? Array.Empty<FilterField>();
} }
} }
} }

56
backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs

@ -1,22 +1,20 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using NJsonSchema;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
namespace Squidex.Infrastructure.Queries.Json namespace Squidex.Infrastructure.Queries.Json
{ {
public static class QueryParser public static class QueryParser
{ {
public static ClrQuery Parse(this JsonSchema schema, string json, IJsonSerializer jsonSerializer) public static ClrQuery Parse(this QueryModel model, string json, IJsonSerializer jsonSerializer)
{ {
if (string.IsNullOrWhiteSpace(json)) if (string.IsNullOrWhiteSpace(json))
{ {
@ -25,10 +23,10 @@ namespace Squidex.Infrastructure.Queries.Json
var query = ParseFromJson(json, jsonSerializer); var query = ParseFromJson(json, jsonSerializer);
return Convert(schema, query); return Convert(model, query);
} }
public static ClrQuery Convert(this JsonSchema schema, Query<IJsonValue> query) public static ClrQuery Convert(this QueryModel model, Query<IJsonValue> query)
{ {
if (query == null) if (query == null)
{ {
@ -39,8 +37,8 @@ namespace Squidex.Infrastructure.Queries.Json
var errors = new List<string>(); var errors = new List<string>();
ConvertSorting(schema, result, errors); model.ConvertSorting(result, errors);
ConvertFilters(schema, result, errors, query); model.ConvertFilters(result, errors, query);
if (errors.Count > 0) if (errors.Count > 0)
{ {
@ -50,34 +48,31 @@ namespace Squidex.Infrastructure.Queries.Json
return result; return result;
} }
private static ValidationError BuildError(string message) private static void ConvertFilters(this QueryModel model, ClrQuery result, List<string> errors, Query<IJsonValue> query)
{ {
var error = T.Get("exception.invalidJsonQuery", new { message }); if (query.Filter == null)
{
return;
}
return new ValidationError(error); var filter = JsonFilterVisitor.Parse(query.Filter, model, errors);
}
private static void ConvertFilters(JsonSchema schema, ClrQuery result, List<string> errors, Query<IJsonValue> query) if (filter != null)
{
if (query.Filter != null)
{ {
var filter = JsonFilterVisitor.Parse(query.Filter, schema, errors); result.Filter = Optimizer<ClrValue>.Optimize(filter);
if (filter != null)
{
result.Filter = Optimizer<ClrValue>.Optimize(filter);
}
} }
} }
private static void ConvertSorting(JsonSchema schema, ClrQuery result, List<string> errors) private static void ConvertSorting(this QueryModel model, ClrQuery result, List<string> errors)
{ {
if (result.Sort != null) if (result.Sort == null)
{ {
foreach (var sorting in result.Sort) return;
{ }
sorting.Path.TryGetProperty(schema, errors, out _);
} foreach (var sorting in result.Sort)
{
sorting.Path.GetMatchingFields(model.Schema, errors);
} }
} }
@ -89,10 +84,15 @@ namespace Squidex.Infrastructure.Queries.Json
} }
catch (JsonException ex) catch (JsonException ex)
{ {
var error = T.Get("exception.invalidJsonQueryJson", new { message = ex.Message }); var error = Errors.InvalidQueryJson(ex.Message);
throw new ValidationException(error); throw new ValidationException(error);
} }
} }
private static ValidationError BuildError(string message)
{
return new ValidationError(Errors.InvalidQuery(message));
}
} }
} }

122
backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs

@ -5,10 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using NJsonSchema;
using NodaTime; using NodaTime;
using NodaTime.Text; using NodaTime.Text;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Infrastructure.Queries.Json namespace Squidex.Infrastructure.Queries.Json
@ -24,13 +22,20 @@ namespace Squidex.Infrastructure.Queries.Json
InstantPattern.CreateWithInvariantCulture("yyyy-MM-dd") InstantPattern.CreateWithInvariantCulture("yyyy-MM-dd")
}; };
public static ClrValue? Convert(JsonSchema schema, IJsonValue value, PropertyPath path, List<string> errors) public static ClrValue? Convert(FilterField field, IJsonValue value, PropertyPath path, List<string> errors)
{ {
ClrValue? result = null; ClrValue? result = null;
switch (GetType(schema)) var type = field.Schema.Type;
if (value is JsonNull && type != FilterSchemaType.GeoObject && field.IsNullable)
{
return ClrValue.Null;
}
switch (type)
{ {
case JsonObjectType.None when schema.Reference?.Format == GeoJson.Format: case FilterSchemaType.GeoObject:
{ {
if (TryParseGeoJson(errors, path, value, out var temp)) if (TryParseGeoJson(errors, path, value, out var temp))
{ {
@ -40,7 +45,7 @@ namespace Squidex.Infrastructure.Queries.Json
break; break;
} }
case JsonObjectType.None: case FilterSchemaType.Any:
{ {
if (value is JsonArray jsonArray) if (value is JsonArray jsonArray)
{ {
@ -56,7 +61,7 @@ namespace Squidex.Infrastructure.Queries.Json
break; break;
} }
case JsonObjectType.Boolean: case FilterSchemaType.Boolean:
{ {
if (value is JsonArray jsonArray) if (value is JsonArray jsonArray)
{ {
@ -70,8 +75,7 @@ namespace Squidex.Infrastructure.Queries.Json
break; break;
} }
case JsonObjectType.Integer: case FilterSchemaType.Number:
case JsonObjectType.Number:
{ {
if (value is JsonArray jsonArray) if (value is JsonArray jsonArray)
{ {
@ -85,48 +89,42 @@ namespace Squidex.Infrastructure.Queries.Json
break; break;
} }
case JsonObjectType.String: case FilterSchemaType.Guid:
{ {
if (schema.Format == JsonFormatStrings.Guid) if (value is JsonArray jsonArray)
{ {
if (value is JsonArray jsonArray) result = ParseArray<Guid>(errors, path, jsonArray, TryParseGuid);
{
result = ParseArray<Guid>(errors, path, jsonArray, TryParseGuid);
}
else if (TryParseGuid(errors, path, value, out var temp))
{
result = temp;
}
} }
else if (schema.Format == JsonFormatStrings.DateTime) else if (TryParseGuid(errors, path, value, out var temp))
{ {
if (value is JsonArray jsonArray) result = temp;
{
result = ParseArray<Instant>(errors, path, jsonArray, TryParseDateTime);
}
else if (TryParseDateTime(errors, path, value, out var temp))
{
result = temp;
}
} }
else
break;
}
case FilterSchemaType.DateTime:
{
if (value is JsonArray jsonArray)
{ {
if (value is JsonArray jsonArray) result = ParseArray<Instant>(errors, path, jsonArray, TryParseDateTime);
{ }
result = ParseArray<string>(errors, path, jsonArray, TryParseString!); else if (TryParseDateTime(errors, path, value, out var temp))
} {
else if (TryParseString(errors, path, value, out var temp)) result = temp;
{
result = temp;
}
} }
break; break;
} }
case JsonObjectType.Object when schema.Format == GeoJson.Format || schema.Reference?.Format == GeoJson.Format: case FilterSchemaType.StringArray:
case FilterSchemaType.String:
{ {
if (TryParseGeoJson(errors, path, value, out var temp)) if (value is JsonArray jsonArray)
{
result = ParseArray<string>(errors, path, jsonArray, TryParseString!);
}
else if (TryParseString(errors, path, value, out var temp))
{ {
result = temp; result = temp;
} }
@ -136,7 +134,7 @@ namespace Squidex.Infrastructure.Queries.Json
default: default:
{ {
errors.Add($"Unsupported type {schema.Type} for {path}."); errors.Add(Errors.WrongType(type.ToString(), path));
break; break;
} }
} }
@ -161,6 +159,8 @@ namespace Squidex.Infrastructure.Queries.Json
private static bool TryParseGeoJson(List<string> errors, PropertyPath path, IJsonValue value, out FilterSphere result) private static bool TryParseGeoJson(List<string> errors, PropertyPath path, IJsonValue value, out FilterSphere result)
{ {
const string expected = "Object(geo-json)";
result = default!; result = default!;
if (value is JsonObject geoObject && if (value is JsonObject geoObject &&
@ -173,13 +173,15 @@ namespace Squidex.Infrastructure.Queries.Json
return true; return true;
} }
errors.Add($"Expected Object(geo-json) for path '{path}', but got {value.Type}."); errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path));
return false; return false;
} }
private static bool TryParseBoolean(List<string> errors, PropertyPath path, IJsonValue value, out bool result) private static bool TryParseBoolean(List<string> errors, PropertyPath path, IJsonValue value, out bool result)
{ {
const string expected = "Boolean";
result = default; result = default;
if (value is JsonBoolean jsonBoolean) if (value is JsonBoolean jsonBoolean)
@ -189,13 +191,15 @@ namespace Squidex.Infrastructure.Queries.Json
return true; return true;
} }
errors.Add($"Expected Boolean for path '{path}', but got {value.Type}."); errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path));
return false; return false;
} }
private static bool TryParseNumber(List<string> errors, PropertyPath path, IJsonValue value, out double result) private static bool TryParseNumber(List<string> errors, PropertyPath path, IJsonValue value, out double result)
{ {
const string expected = "Number";
result = default; result = default;
if (value is JsonNumber jsonNumber) if (value is JsonNumber jsonNumber)
@ -205,13 +209,15 @@ namespace Squidex.Infrastructure.Queries.Json
return true; return true;
} }
errors.Add($"Expected Number for path '{path}', but got {value.Type}."); errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path));
return false; return false;
} }
private static bool TryParseString(List<string> errors, PropertyPath path, IJsonValue value, out string? result) private static bool TryParseString(List<string> errors, PropertyPath path, IJsonValue value, out string? result)
{ {
const string expected = "String";
result = default; result = default;
if (value is JsonString jsonString) if (value is JsonString jsonString)
@ -220,18 +226,16 @@ namespace Squidex.Infrastructure.Queries.Json
return true; return true;
} }
else if (value is JsonNull)
{
return true;
}
errors.Add($"Expected String for path '{path}', but got {value.Type}."); errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path));
return false; return false;
} }
private static bool TryParseGuid(List<string> errors, PropertyPath path, IJsonValue value, out Guid result) private static bool TryParseGuid(List<string> errors, PropertyPath path, IJsonValue value, out Guid result)
{ {
const string expected = "String (Guid)";
result = default; result = default;
if (value is JsonString jsonString) if (value is JsonString jsonString)
@ -241,11 +245,11 @@ namespace Squidex.Infrastructure.Queries.Json
return true; return true;
} }
errors.Add($"Expected Guid String for path '{path}', but got invalid String."); errors.Add(Errors.WrongFormat(expected, path));
} }
else else
{ {
errors.Add($"Expected Guid String for path '{path}', but got {value.Type}."); errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path));
} }
return false; return false;
@ -253,6 +257,8 @@ namespace Squidex.Infrastructure.Queries.Json
private static bool TryParseDateTime(List<string> errors, PropertyPath path, IJsonValue value, out Instant result) private static bool TryParseDateTime(List<string> errors, PropertyPath path, IJsonValue value, out Instant result)
{ {
const string expected = "String (ISO8601 DateTime)";
result = default; result = default;
if (value is JsonString jsonString) if (value is JsonString jsonString)
@ -269,11 +275,11 @@ namespace Squidex.Infrastructure.Queries.Json
} }
} }
errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got invalid String."); errors.Add(Errors.WrongFormat(expected, path));
} }
else else
{ {
errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got {value.Type}."); errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path));
} }
return false; return false;
@ -320,19 +326,9 @@ namespace Squidex.Infrastructure.Queries.Json
} }
} }
errors.Add($"Expected primitive for path '{path}', but got {value.Type}."); errors.Add(Errors.WrongPrimitive(value.Type.ToString(), path));
return false; return false;
} }
private static JsonObjectType GetType(JsonSchema schema)
{
if (schema.Item != null)
{
return schema.Item.Type;
}
return schema.Type;
}
} }
} }

125
backend/src/Squidex.Infrastructure/Queries/OData/EdmModelConverter.cs

@ -0,0 +1,125 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData.Edm;
using Squidex.Text;
namespace Squidex.Infrastructure.Queries.OData
{
public static class EdmModelConverter
{
private const int MaxDepth = 7;
public static EdmModel ConvertToEdm(this QueryModel queryModel, string modelName, string name)
{
var model = new EdmModel();
var entityType = new EdmEntityType(modelName, name);
var entityPath = new Stack<string>();
void Convert(EdmStructuredType target, FilterSchema schema)
{
if (schema.Fields == null)
{
return;
}
foreach (var field in FilterSchema.GetConflictFreeFields(schema.Fields))
{
var fieldName = field.Path.EscapeEdmField();
switch (field.Schema.Type)
{
case FilterSchemaType.Boolean:
target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.Boolean, field.IsNullable);
break;
case FilterSchemaType.DateTime:
target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.DateTimeOffset, field.IsNullable);
break;
case FilterSchemaType.GeoObject:
target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.GeographyPoint, field.IsNullable);
break;
case FilterSchemaType.Guid:
target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.Guid, field.IsNullable);
break;
case FilterSchemaType.Number:
target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.Double, field.IsNullable);
break;
case FilterSchemaType.String:
case FilterSchemaType.StringArray:
target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.String, field.IsNullable);
break;
case FilterSchemaType.Object:
case FilterSchemaType.ObjectArray:
{
if (field.Schema.Fields == null || field.Schema.Fields.Count == 0 || entityPath.Count >= MaxDepth)
{
break;
}
entityPath.Push(fieldName);
var typeName = string.Join("_", entityPath.Reverse().Select(x => x.EscapeEdmField().ToPascalCase()));
var result = model.SchemaElements.OfType<EdmComplexType>().FirstOrDefault(x => x.Name == typeName);
if (result == null)
{
result = new EdmComplexType(modelName, typeName);
model.AddElement(result);
Convert(result, field.Schema);
}
target.AddStructuralProperty(fieldName, new EdmComplexTypeReference(result, field.IsNullable));
entityPath.Pop();
break;
}
case FilterSchemaType.Any:
{
var result = model.SchemaElements.OfType<EdmComplexType>().FirstOrDefault(x => x.Name == "Any");
if (result == null)
{
result = new EdmComplexType("Squidex", "Any", null, false, true);
model.AddElement(result);
}
target.AddStructuralProperty(fieldName, new EdmComplexTypeReference(result, field.IsNullable));
break;
}
}
}
}
Convert(entityType, queryModel.Schema);
var container = new EdmEntityContainer("Squidex", "Container");
container.AddEntitySet("ContentSet", entityType);
model.AddElement(container);
model.AddElement(entityType);
return model;
}
public static string EscapeEdmField(this string field)
{
return field.Replace("-", "_", StringComparison.Ordinal);
}
public static string UnescapeEdmField(this string field)
{
return field.Replace("_", "-", StringComparison.Ordinal);
}
}
}

136
backend/src/Squidex.Infrastructure/Queries/QueryModel.cs

@ -0,0 +1,136 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public sealed class QueryModel
{
public static readonly IReadOnlyDictionary<FilterSchemaType, IReadOnlyList<CompareOperator>> DefaultOperators = new Dictionary<FilterSchemaType, IReadOnlyList<CompareOperator>>
{
[FilterSchemaType.Any] = Enum.GetValues(typeof(CompareOperator)).OfType<CompareOperator>().ToList(),
[FilterSchemaType.Boolean] = new List<CompareOperator>
{
CompareOperator.Equals,
CompareOperator.Exists,
CompareOperator.In,
CompareOperator.NotEquals
},
[FilterSchemaType.DateTime] = new List<CompareOperator>
{
CompareOperator.Contains,
CompareOperator.Empty,
CompareOperator.Exists,
CompareOperator.EndsWith,
CompareOperator.Equals,
CompareOperator.GreaterThan,
CompareOperator.GreaterThanOrEqual,
CompareOperator.In,
CompareOperator.LessThan,
CompareOperator.LessThanOrEqual,
CompareOperator.Matchs,
CompareOperator.NotEquals,
CompareOperator.StartsWith
},
[FilterSchemaType.GeoObject] = new List<CompareOperator>
{
CompareOperator.LessThan,
CompareOperator.Exists
},
[FilterSchemaType.Guid] = new List<CompareOperator>
{
CompareOperator.Contains,
CompareOperator.Empty,
CompareOperator.Exists,
CompareOperator.EndsWith,
CompareOperator.Equals,
CompareOperator.GreaterThan,
CompareOperator.GreaterThanOrEqual,
CompareOperator.In,
CompareOperator.LessThan,
CompareOperator.LessThanOrEqual,
CompareOperator.Matchs,
CompareOperator.NotEquals,
CompareOperator.StartsWith
},
[FilterSchemaType.Object] = new List<CompareOperator>(),
[FilterSchemaType.ObjectArray] = new List<CompareOperator>
{
CompareOperator.Empty,
CompareOperator.Exists,
CompareOperator.Equals,
CompareOperator.In,
CompareOperator.NotEquals
},
[FilterSchemaType.Number] = new List<CompareOperator>
{
CompareOperator.Equals,
CompareOperator.Exists,
CompareOperator.LessThan,
CompareOperator.LessThanOrEqual,
CompareOperator.GreaterThan,
CompareOperator.GreaterThanOrEqual,
CompareOperator.In,
CompareOperator.NotEquals
},
[FilterSchemaType.String] = new List<CompareOperator>
{
CompareOperator.Contains,
CompareOperator.Empty,
CompareOperator.Exists,
CompareOperator.EndsWith,
CompareOperator.Equals,
CompareOperator.GreaterThan,
CompareOperator.GreaterThanOrEqual,
CompareOperator.In,
CompareOperator.LessThan,
CompareOperator.LessThanOrEqual,
CompareOperator.Matchs,
CompareOperator.NotEquals,
CompareOperator.StartsWith
},
[FilterSchemaType.StringArray] = new List<CompareOperator>
{
CompareOperator.Contains,
CompareOperator.Empty,
CompareOperator.Exists,
CompareOperator.EndsWith,
CompareOperator.Equals,
CompareOperator.GreaterThan,
CompareOperator.GreaterThanOrEqual,
CompareOperator.In,
CompareOperator.LessThan,
CompareOperator.LessThanOrEqual,
CompareOperator.Matchs,
CompareOperator.NotEquals,
CompareOperator.StartsWith
}
};
public FilterSchema Schema { get; init; } = FilterSchema.Any;
public IReadOnlyDictionary<FilterSchemaType, IReadOnlyList<CompareOperator>> Operators { get; init; } = DefaultOperators;
public QueryModel Flatten(int maxDepth = 7, bool onlyWithOperators = true)
{
var predicate = (Predicate<FilterSchema>?)null;
if (onlyWithOperators)
{
predicate = x => Operators.TryGetValue(x.Type, out var operators) && operators.Count > 0;
}
var flatten = Schema.Flatten(maxDepth, predicate);
if (ReferenceEquals(flatten, Schema))
{
return this;
}
return new QueryModel { Operators = Operators, Schema = flatten };
}
}
}

14
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -28,7 +28,6 @@
<PackageReference Include="Microsoft.Orleans.Core" Version="3.5.1" /> <PackageReference Include="Microsoft.Orleans.Core" Version="3.5.1" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.5.1" /> <PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.5.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NJsonSchema" Version="10.6.6" />
<PackageReference Include="OpenTelemetry.Api" Version="1.1.0" /> <PackageReference Include="OpenTelemetry.Api" Version="1.1.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="2.6.0" /> <PackageReference Include="Squidex.Assets" Version="2.6.0" />
@ -49,4 +48,17 @@
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> <AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project> </Project>

19
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -11,12 +11,12 @@ using Microsoft.Net.Http.Headers;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Assets.Models; using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Assets; using Squidex.Assets;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Apps.Plans;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Scripting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
@ -387,8 +387,21 @@ namespace Squidex.Areas.Api.Controllers.Assets
[OpenApiIgnore] [OpenApiIgnore]
public IActionResult GetScriptCompletion(string app, string schema) public IActionResult GetScriptCompletion(string app, string schema)
{ {
var completer = new ScriptingCompletion(); var completer = HttpContext.RequestServices.GetRequiredService<ScriptingCompleter>();
var completion = completer.Asset(); var completion = completer.AssetScript();
return Ok(completion);
}
[HttpGet]
[Route("apps/{app}/assets/completion/trigger")]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[OpenApiIgnore]
public IActionResult GetScriptTriggerCompletion(string app, string schema)
{
var completer = HttpContext.RequestServices.GetRequiredService<ScriptingCompleter>();
var completion = completer.AssetTrigger();
return Ok(completion); return Ok(completion);
} }

4
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs

@ -52,7 +52,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
var contentSchema = RegisterReference("ContentDto", _ => var contentSchema = RegisterReference("ContentDto", _ =>
{ {
return ContentJsonSchemaBuilder.BuildSchema(dataSchema, true); return ContentJsonSchema.Build(dataSchema, true);
}); });
var contentsSchema = RegisterReference("ContentResultDto", _ => var contentsSchema = RegisterReference("ContentResultDto", _ =>
@ -95,7 +95,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
var contentSchema = RegisterReference($"{typeName}ContentDto", _ => var contentSchema = RegisterReference($"{typeName}ContentDto", _ =>
{ {
return ContentJsonSchemaBuilder.BuildSchema(flat ? flatDataSchema : dataSchema, true); return ContentJsonSchema.Build(flat ? flatDataSchema : dataSchema, true);
}); });
var contentsSchema = RegisterReference($"{typeName}ContentResultDto", _ => var contentsSchema = RegisterReference($"{typeName}ContentResultDto", _ =>

44
backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -9,13 +9,15 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Core.GenerateFilters;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Scripting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Queries;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
@ -333,14 +335,48 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
[OpenApiIgnore] [OpenApiIgnore]
public IActionResult GetScriptCompletion(string app, string schema) public async Task<IActionResult> GetScriptCompletion(string app, string schema)
{ {
var completer = new ScriptingCompletion(); var completer = HttpContext.RequestServices.GetRequiredService<ScriptingCompleter>();
var completion = completer.Content(Schema.SchemaDef, App.PartitionResolver()); var completion = completer.ContentScript(await BuildModel());
return Ok(completion); return Ok(completion);
} }
[HttpGet]
[Route("apps/{app}/schemas/{schema}/completion/triggers")]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[OpenApiIgnore]
public async Task<IActionResult> GetScriptTriggerCompletion(string app, string schema)
{
var completer = HttpContext.RequestServices.GetRequiredService<ScriptingCompleter>();
var completion = completer.ContentTrigger(await BuildModel());
return Ok(completion);
}
[HttpGet]
[Route("apps/{app}/schemas/{schema}/filters")]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[OpenApiIgnore]
public async Task<IActionResult> GetFilters(string app, string schema)
{
var components = await appProvider.GetComponentsAsync(Schema, HttpContext.RequestAborted);
var filters = ContentQueryModel.Build(Schema.SchemaDef, App.PartitionResolver(), components).Flatten();
return Ok(filters);
}
private async Task<FilterSchema> BuildModel()
{
var components = await appProvider.GetComponentsAsync(Schema, HttpContext.RequestAborted);
return Schema.SchemaDef.BuildDataSchema(App.PartitionResolver(), components).Flatten();
}
private Task<ISchemaEntity?> GetSchemaAsync(string schema) private Task<ISchemaEntity?> GetSchemaAsync(string schema)
{ {
if (Guid.TryParse(schema, out var guid)) if (Guid.TryParse(schema, out var guid))

3
backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/ConnectController.cs

@ -17,6 +17,7 @@ using Squidex.Areas.IdentityServer.Config;
using Squidex.Areas.IdentityServer.Controllers; using Squidex.Areas.IdentityServer.Controllers;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using Squidex.Shared.Users;
using Squidex.Web; using Squidex.Web;
using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictConstants;
@ -165,7 +166,7 @@ namespace Notifo.Areas.Account.Controllers
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
} }
private async Task<ClaimsPrincipal> CreatePrincipalAsync(OpenIddictRequest request, Squidex.Shared.Users.IUser? user) private async Task<ClaimsPrincipal> CreatePrincipalAsync(OpenIddictRequest request, IUser user)
{ {
var principal = await SignInManager.CreateUserPrincipalAsync((IdentityUser)user.Identity); var principal = await SignInManager.CreateUserPrincipalAsync((IdentityUser)user.Identity);

13
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -64,6 +64,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<BackgroundRequestLogStore>() services.AddSingletonAs<BackgroundRequestLogStore>()
.AsOptional<IRequestLogStore>(); .AsOptional<IRequestLogStore>();
services.AddSingletonAs<ScriptingCompleter>()
.AsSelf();
services.AddSingletonAs<JintScriptEngine>() services.AddSingletonAs<JintScriptEngine>()
.As<IScriptEngine>(); .As<IScriptEngine>();
@ -71,19 +74,19 @@ namespace Squidex.Config.Domain
.As<ITagService>(); .As<ITagService>();
services.AddSingletonAs<CounterJintExtension>() services.AddSingletonAs<CounterJintExtension>()
.As<IJintExtension>(); .As<IJintExtension>().As<IScriptDescriptor>();
services.AddSingletonAs<DateTimeJintExtension>() services.AddSingletonAs<DateTimeJintExtension>()
.As<IJintExtension>(); .As<IJintExtension>().As<IScriptDescriptor>();
services.AddSingletonAs<StringJintExtension>() services.AddSingletonAs<StringJintExtension>()
.As<IJintExtension>(); .As<IJintExtension>().As<IScriptDescriptor>();
services.AddSingletonAs<StringWordsJintExtension>() services.AddSingletonAs<StringWordsJintExtension>()
.As<IJintExtension>(); .As<IJintExtension>().As<IScriptDescriptor>();
services.AddSingletonAs<HttpJintExtension>() services.AddSingletonAs<HttpJintExtension>()
.As<IJintExtension>(); .As<IJintExtension>().As<IScriptDescriptor>();
services.AddSingletonAs<FluidTemplateEngine>() services.AddSingletonAs<FluidTemplateEngine>()
.AsOptional<ITemplateEngine>(); .AsOptional<ITemplateEngine>();

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

@ -51,13 +51,13 @@ namespace Squidex.Config.Domain
.As<IFluidExtension>(); .As<IFluidExtension>();
services.AddSingletonAs<AssetsJintExtension>() services.AddSingletonAs<AssetsJintExtension>()
.As<IJintExtension>(); .As<IJintExtension>().As<IScriptDescriptor>();
services.AddSingletonAs<ReferencesFluidExtension>() services.AddSingletonAs<ReferencesFluidExtension>()
.As<IFluidExtension>(); .As<IFluidExtension>();
services.AddSingletonAs<ReferencesJintExtension>() services.AddSingletonAs<ReferencesJintExtension>()
.As<IJintExtension>(); .As<IJintExtension>().As<IScriptDescriptor>();
services.AddSingletonAs<ManualTriggerHandler>() services.AddSingletonAs<ManualTriggerHandler>()
.As<IRuleTriggerHandler>(); .As<IRuleTriggerHandler>();
@ -87,7 +87,7 @@ namespace Squidex.Config.Domain
.As<ITypeProvider>().AsSelf(); .As<ITypeProvider>().AsSelf();
services.AddSingletonAs<EventJintExtension>() services.AddSingletonAs<EventJintExtension>()
.As<IJintExtension>(); .As<IJintExtension>().As<IScriptDescriptor>();
services.AddSingletonAs<EventFluidExtensions>() services.AddSingletonAs<EventFluidExtensions>()
.As<IFluidExtension>(); .As<IFluidExtension>();

2
backend/src/Squidex/Config/Domain/SerializationServices.cs

@ -29,6 +29,7 @@ using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Newtonsoft; using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.Json;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
@ -41,6 +42,7 @@ namespace Squidex.Config.Domain
settings.Converters.Add(new StringEnumConverter()); settings.Converters.Add(new StringEnumConverter());
settings.ContractResolver = new ConverterContractResolver( settings.ContractResolver = new ConverterContractResolver(
new CompareOperatorJsonConverter(),
new ContentFieldDataConverter(), new ContentFieldDataConverter(),
new EnvelopeHeadersConverter(), new EnvelopeHeadersConverter(),
new ExecutionResultJsonConverter(new ErrorInfoProvider()), new ExecutionResultJsonConverter(new ErrorInfoProvider()),

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs

@ -455,7 +455,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
public void Should_serialize_and_deserialize_schema() public void Should_serialize_and_deserialize_schema()
{ {
var schemaSource = var schemaSource =
TestUtils.MixedSchema(SchemaType.Singleton) TestUtils.MixedSchema(SchemaType.Singleton).Schema
.ChangeCategory("Category") .ChangeCategory("Category")
.SetFieldRules(FieldRule.Hide("2")) .SetFieldRules(FieldRule.Hide("2"))
.SetFieldsInLists("field2") .SetFieldsInLists("field2")

49
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs

@ -1,49 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData.Edm;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.GenerateEdmSchema
{
public class EdmTests
{
[Fact]
public void Should_escape_field_name()
{
Assert.Equal("field_name", "field-name".EscapeEdmField());
}
[Fact]
public void Should_unescape_field_name()
{
Assert.Equal("field-name", "field_name".UnescapeEdmField());
}
[Fact]
public void Should_build_edm_model()
{
var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var typeFactory = new EdmTypeFactory(names =>
{
return (new EdmComplexType("Squidex", string.Join(".", names)), true);
});
var edmModel =
TestUtils.MixedSchema()
.BuildEdmType(true, languagesConfig.ToResolver(), typeFactory, ResolvedComponents.Empty);
Assert.NotNull(edmModel);
}
}
}

104
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs

@ -0,0 +1,104 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.GenerateFilters;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.GenerateFilters
{
public class FiltersTests
{
[Fact]
public void Should_build_content_query_model()
{
var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var (schema, components) = TestUtils.MixedSchema();
var queryModel = ContentQueryModel.Build(schema, languagesConfig.ToResolver(), components);
Assert.NotNull(queryModel);
}
[Fact]
public void Should_build_dynamic_content_query_model()
{
var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var queryModel = ContentQueryModel.Build(null, languagesConfig.ToResolver(), ResolvedComponents.Empty);
Assert.NotNull(queryModel);
}
[Fact]
public void Should_build_asset_query_model()
{
var queryModel = AssetQueryModel.Build();
Assert.NotNull(queryModel);
}
private static void CheckFields(FilterSchema filterSchema, Schema schema)
{
var filterProperties = AllPropertyNames(filterSchema);
void CheckField(IField field)
{
if (!field.IsForApi())
{
Assert.DoesNotContain(field.Name, filterProperties);
}
else
{
Assert.Contains(field.Name, filterProperties);
}
if (field is IArrayField array)
{
foreach (var nested in array.Fields)
{
CheckField(nested);
}
}
}
foreach (var field in schema.Fields)
{
CheckField(field);
}
}
private static HashSet<string> AllPropertyNames(FilterSchema schema)
{
var result = new HashSet<string>();
void AddProperties(FilterSchema current)
{
if (current == null)
{
return;
}
foreach (var field in current.Fields.OrEmpty())
{
result.Add(field.Path);
AddProperties(field.Schema);
}
}
AddProperties(schema);
return result;
}
}
}

47
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs

@ -17,16 +17,16 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
{ {
public class JsonSchemaTests public class JsonSchemaTests
{ {
private readonly Schema schema = TestUtils.MixedSchema();
[Fact] [Fact]
public void Should_build_json_schema() public void Should_build_json_schema()
{ {
var languagesConfig = LanguagesConfig.English.Set(Language.DE); var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), ResolvedComponents.Empty); var (schema, components) = TestUtils.MixedSchema();
var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), components);
CheckFields(jsonSchema); CheckFields(jsonSchema, schema);
} }
[Fact] [Fact]
@ -34,9 +34,11 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
{ {
var languagesConfig = LanguagesConfig.English.Set(Language.DE); var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var jsonSchema = schema.BuildJsonSchemaDynamic(languagesConfig.ToResolver(), ResolvedComponents.Empty); var (schema, components) = TestUtils.MixedSchema();
var jsonSchema = schema.BuildJsonSchemaDynamic(languagesConfig.ToResolver(), components);
CheckFields(jsonSchema); CheckFields(jsonSchema, schema);
} }
[Fact] [Fact]
@ -44,12 +46,14 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
{ {
var languagesConfig = LanguagesConfig.English.Set(Language.DE); var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var jsonSchema = schema.BuildJsonSchemaFlat(languagesConfig.ToResolver(), ResolvedComponents.Empty); var (schema, components) = TestUtils.MixedSchema();
CheckFields(jsonSchema); var jsonSchema = schema.BuildJsonSchemaFlat(languagesConfig.ToResolver(), components);
CheckFields(jsonSchema, schema);
} }
private void CheckFields(JsonSchema jsonSchema) private static void CheckFields(JsonSchema jsonSchema, Schema schema)
{ {
var jsonProperties = AllPropertyNames(jsonSchema); var jsonProperties = AllPropertyNames(jsonSchema);
@ -85,23 +89,22 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
void AddProperties(JsonSchema current) void AddProperties(JsonSchema current)
{ {
if (current != null) if (current == null)
{ {
if (current.Properties != null) return;
{ }
foreach (var (key, value) in current.Properties)
{
result.Add(key);
AddProperties(value); foreach (var (key, value) in current.Properties.OrEmpty())
} {
} result.Add(key);
AddProperties(current.Item); AddProperties(value);
AddProperties(current.Reference);
AddProperties(current.AdditionalItemsSchema);
AddProperties(current.AdditionalPropertiesSchema);
} }
AddProperties(current.Item);
AddProperties(current.Reference);
AddProperties(current.AdditionalItemsSchema);
AddProperties(current.AdditionalPropertiesSchema);
} }
AddProperties(schema); AddProperties(schema);

67
backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs

@ -25,6 +25,7 @@ using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Newtonsoft; using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.Json;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.TestHelpers namespace Squidex.Domain.Apps.Core.TestHelpers
@ -49,6 +50,7 @@ namespace Squidex.Domain.Apps.Core.TestHelpers
SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry), SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry),
ContractResolver = new ConverterContractResolver( ContractResolver = new ConverterContractResolver(
new CompareOperatorJsonConverter(),
new ContentFieldDataConverter(), new ContentFieldDataConverter(),
new EnvelopeHeadersConverter(), new EnvelopeHeadersConverter(),
new JsonValueConverter(), new JsonValueConverter(),
@ -82,24 +84,57 @@ namespace Squidex.Domain.Apps.Core.TestHelpers
return new NewtonsoftJsonSerializer(serializerSettings); return new NewtonsoftJsonSerializer(serializerSettings);
} }
public static Schema MixedSchema(SchemaType type = SchemaType.Default) public static (Schema Schema, ResolvedComponents) MixedSchema(SchemaType type = SchemaType.Default)
{ {
var componentId = DomainId.NewGuid(); var componentId1 = DomainId.NewGuid();
var componentId2 = DomainId.NewGuid();
var componentIds = ReadonlyList.Create(componentId1, componentId2);
var component1 = new Schema("component1")
.Publish()
.AddString(1, "unique1", Partitioning.Invariant)
.AddString(2, "shared1", Partitioning.Invariant)
.AddBoolean(3, "shared2", Partitioning.Invariant);
var component2 = new Schema("component2")
.Publish()
.AddNumber(1, "unique1", Partitioning.Invariant)
.AddNumber(2, "shared1", Partitioning.Invariant)
.AddBoolean(3, "shared2", Partitioning.Invariant);
var resolvedComponents = new ResolvedComponents(new Dictionary<DomainId, Schema>
{
[componentId1] = component1,
[componentId2] = component2,
});
var schema = new Schema("user", type: type) var schema = new Schema("user", type: type)
.Publish() .Publish()
.AddArray(101, "root-array", Partitioning.Language, f => f .AddArray(101, "root-array", Partitioning.Language, f => f
.AddAssets(201, "nested-assets") .AddAssets(201, "nested-assets",
.AddBoolean(202, "nested-boolean") new AssetsFieldProperties())
.AddDateTime(203, "nested-datetime") .AddBoolean(202, "nested-boolean",
.AddGeolocation(204, "nested-geolocation") new BooleanFieldProperties())
.AddJson(205, "nested-json") .AddDateTime(203, "nested-datetime",
.AddJson(211, "nested-json2") new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime })
.AddNumber(206, "nested-number") .AddDateTime(204, "nested-date",
.AddReferences(207, "nested-references") new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })
.AddString(208, "nested-string") .AddGeolocation(205, "nested-geolocation",
.AddTags(209, "nested-tags") new GeolocationFieldProperties())
.AddUI(210, "nested-ui")) .AddJson(206, "nested-json",
new JsonFieldProperties())
.AddJson(207, "nested-json2",
new JsonFieldProperties())
.AddNumber(208, "nested-number",
new NumberFieldProperties())
.AddReferences(209, "nested-references",
new ReferencesFieldProperties())
.AddString(210, "nested-string",
new StringFieldProperties())
.AddTags(211, "nested-tags",
new TagsFieldProperties())
.AddUI(212, "nested-ui",
new UIFieldProperties()))
.AddAssets(102, "root-assets", Partitioning.Invariant, .AddAssets(102, "root-assets", Partitioning.Invariant,
new AssetsFieldProperties()) new AssetsFieldProperties())
.AddBoolean(103, "root-boolean", Partitioning.Invariant, .AddBoolean(103, "root-boolean", Partitioning.Invariant,
@ -125,9 +160,9 @@ namespace Squidex.Domain.Apps.Core.TestHelpers
.AddUI(113, "root-ui", Partitioning.Language, .AddUI(113, "root-ui", Partitioning.Language,
new UIFieldProperties()) new UIFieldProperties())
.AddComponent(114, "root-component", Partitioning.Language, .AddComponent(114, "root-component", Partitioning.Language,
new ComponentFieldProperties { SchemaId = componentId }) new ComponentFieldProperties { SchemaIds = componentIds })
.AddComponents(115, "root-components", Partitioning.Language, .AddComponents(115, "root-components", Partitioning.Language,
new ComponentsFieldProperties { SchemaId = componentId }) new ComponentsFieldProperties { SchemaIds = componentIds })
.Update(new SchemaProperties { Hints = "The User" }) .Update(new SchemaProperties { Hints = "The User" })
.HideField(104) .HideField(104)
.HideField(211, 101) .HideField(211, 101)
@ -135,7 +170,7 @@ namespace Squidex.Domain.Apps.Core.TestHelpers
.DisableField(212, 101) .DisableField(212, 101)
.LockField(105); .LockField(105);
return schema; return (schema, resolvedComponents);
} }
public static T SerializeAndDeserialize<T>(this object value) public static T SerializeAndDeserialize<T>(this object value)

156
backend/tests/Squidex.Infrastructure.Tests/Queries/FilterSchemaTests.cs

@ -0,0 +1,156 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Collections;
using Xunit;
namespace Squidex.Infrastructure.Queries
{
public class FilterSchemaTests
{
[Fact]
public void Should_flatten_schema()
{
var schema = new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(FilterSchema.Number, "nested3")
}.ToReadonlyList()
}, "nested2")
}.ToReadonlyList()
}, "nested1")
}.ToReadonlyList()
};
var expected = new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(new FilterSchema(FilterSchemaType.Object), "nested1"),
new FilterField(new FilterSchema(FilterSchemaType.Object), "nested1.nested2"),
new FilterField(new FilterSchema(FilterSchemaType.Number), "nested1.nested2.nested3")
}.ToReadonlyList()
};
var actual = schema.Flatten();
Assert.Equal(expected, actual);
}
[Fact]
public void Should_ignore_conflicts_when_flatten()
{
var schema = new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(FilterSchema.Number, "property1"),
new FilterField(FilterSchema.String, "property1"),
new FilterField(FilterSchema.String, "property2"),
new FilterField(FilterSchema.String, "property2")
}.ToReadonlyList()
};
var expected = new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(FilterSchema.String, "property2")
}.ToReadonlyList()
};
var actual = schema.Flatten();
Assert.Equal(expected, actual);
}
[Fact]
public void Should_filter_out_fields_by_predicate_when_flatten()
{
var schema = new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"),
new FilterField(FilterSchema.String, "property2"),
}.ToReadonlyList()
};
var expected = new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(FilterSchema.String, "property2")
}.ToReadonlyList()
};
var actual = schema.Flatten(predicate: x => x.Type != FilterSchemaType.Object);
Assert.Equal(expected, actual);
}
[Fact]
public void Should_filter_out_fields_without_operators_when_flattened_by_model()
{
var schema = new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"),
new FilterField(FilterSchema.String, "property2"),
}.ToReadonlyList()
};
var expected = new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(FilterSchema.String, "property2")
}.ToReadonlyList()
};
var actual = new QueryModel { Schema = schema }.Flatten().Schema;
Assert.Equal(expected, actual);
}
[Fact]
public void Should_not_filter_out_fields_without_operators_when_flattened_by_model_but_flag_is_false()
{
var schema = new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"),
new FilterField(FilterSchema.String, "property2"),
}.ToReadonlyList()
};
var expected = new FilterSchema(FilterSchemaType.Object)
{
Fields = new[]
{
new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"),
new FilterField(FilterSchema.String, "property2")
}.ToReadonlyList()
};
var actual = new QueryModel { Schema = schema }.Flatten(onlyWithOperators: false).Schema;
Assert.Equal(expected, actual);
}
}
}

280
backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs

@ -5,8 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using NJsonSchema; using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Queries.Json; using Squidex.Infrastructure.Queries.Json;
using Squidex.Infrastructure.TestHelpers; using Squidex.Infrastructure.TestHelpers;
@ -32,80 +31,43 @@ namespace Squidex.Infrastructure.Queries
("StartsWith", "startswith", "startsWith($FIELD, $VALUE)") ("StartsWith", "startswith", "startsWith($FIELD, $VALUE)")
}; };
private static readonly JsonSchema Schema = new JsonSchema(); private static readonly QueryModel Model = new QueryModel();
static QueryFromJsonTests() static QueryFromJsonTests()
{ {
var nested = new JsonSchemaProperty { Title = "nested", Type = JsonObjectType.Object }; var nestedSchema = new FilterSchema(FilterSchemaType.Object)
nested.Properties["property"] = new JsonSchemaProperty
{
Type = JsonObjectType.String
};
Schema.Properties["boolean"] = new JsonSchemaProperty
{
Type = JsonObjectType.Boolean
};
Schema.Properties["datetime"] = new JsonSchemaProperty
{
Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime
};
Schema.Properties["guid"] = new JsonSchemaProperty
{
Type = JsonObjectType.String, Format = JsonFormatStrings.Guid
};
Schema.Properties["integer"] = new JsonSchemaProperty
{
Type = JsonObjectType.Integer
};
Schema.Properties["number"] = new JsonSchemaProperty
{ {
Type = JsonObjectType.Number Fields = ReadonlyList.Create(new FilterField(FilterSchema.String, "property"))
}; };
Schema.Properties["json"] = new JsonSchemaProperty var fields = new List<FilterField>
{ {
Type = JsonObjectType.None new FilterField(nestedSchema, "object"),
}; new FilterField(FilterSchema.Any, "json"),
new FilterField(FilterSchema.Boolean, "boolean"),
Schema.Properties["geo"] = new JsonSchemaProperty new FilterField(FilterSchema.Boolean, "booleanNullable", IsNullable: true),
{ new FilterField(FilterSchema.DateTime, "datetime"),
Type = JsonObjectType.Object, Format = GeoJson.Format new FilterField(FilterSchema.DateTime, "datetimeNullable", IsNullable: true),
}; new FilterField(FilterSchema.GeoObject, "geo"),
new FilterField(FilterSchema.Guid, "guid"),
Schema.Properties["reference"] = new JsonSchemaProperty new FilterField(FilterSchema.Guid, "guidNullable", IsNullable: true),
{ new FilterField(FilterSchema.Number, "number"),
Reference = nested new FilterField(FilterSchema.Number, "numberNullable", IsNullable: true),
}; new FilterField(FilterSchema.Number, "union"),
new FilterField(FilterSchema.String, "string"),
Schema.Properties["string"] = new JsonSchemaProperty new FilterField(FilterSchema.String, "stringNullable", IsNullable: true),
{ new FilterField(FilterSchema.String, "union"),
Type = JsonObjectType.String new FilterField(FilterSchema.StringArray, "stringArray"),
}; new FilterField(FilterSchema.StringArray, "stringArrayNullable", IsNullable: true),
new FilterField(FilterSchema.String, "nested2.value")
Schema.Properties["geoRef"] = new JsonSchemaProperty
{
Reference = new JsonSchema
{
Format = GeoJson.Format
}
}; };
Schema.Properties["stringArray"] = new JsonSchemaProperty var schema = new FilterSchema(FilterSchemaType.Object)
{ {
Item = new JsonSchema Fields = fields.ToReadonlyList()
{
Type = JsonObjectType.String
},
Type = JsonObjectType.Array
}; };
Schema.Properties["object"] = nested; Model = new QueryModel { Schema = schema };
} }
public class DateTime public class DateTime
@ -126,12 +88,28 @@ namespace Squidex.Infrastructure.Queries
AssertFilter(json, expected); AssertFilter(json, expected);
} }
[Fact]
public void Should_parse_filter_with_null()
{
var json = new { path = "datetimeNullable", op = "eq", value = (object?)null };
AssertFilter(json, "datetimeNullable == null");
}
[Fact]
public void Should_add_error_if_field_is_not_nullable()
{
var json = new { path = "datetime", op = "eq", value = (object?)null };
AssertFilterError(json, "Expected String (ISO8601 DateTime) for path 'datetime', but got Null.");
}
[Fact] [Fact]
public void Should_add_error_if_value_is_invalid() public void Should_add_error_if_value_is_invalid()
{ {
var json = new { path = "datetime", op = "eq", value = "invalid" }; var json = new { path = "datetime", op = "eq", value = "invalid" };
AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); AssertFilterError(json, "Expected String (ISO8601 DateTime) for path 'datetime', but got invalid String.");
} }
[Fact] [Fact]
@ -139,7 +117,7 @@ namespace Squidex.Infrastructure.Queries
{ {
var json = new { path = "datetime", op = "eq", value = 1 }; var json = new { path = "datetime", op = "eq", value = 1 };
AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); AssertFilterError(json, "Expected String (ISO8601 DateTime) for path 'datetime', but got Number.");
} }
} }
@ -161,12 +139,28 @@ namespace Squidex.Infrastructure.Queries
AssertFilter(json, expected); AssertFilter(json, expected);
} }
[Fact]
public void Should_parse_filter_with_null()
{
var json = new { path = "guidNullable", op = "eq", value = (object?)null };
AssertFilter(json, "guidNullable == null");
}
[Fact]
public void Should_add_error_if_field_is_not_nullable()
{
var json = new { path = "guid", op = "eq", value = (object?)null };
AssertFilterError(json, "Expected String (Guid) for path 'guid', but got Null.");
}
[Fact] [Fact]
public void Should_add_error_if_value_is_invalid() public void Should_add_error_if_value_is_invalid()
{ {
var json = new { path = "guid", op = "eq", value = "invalid" }; var json = new { path = "guid", op = "eq", value = "invalid" };
AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); AssertFilterError(json, "Expected String (Guid) for path 'guid', but got invalid String.");
} }
[Fact] [Fact]
@ -174,7 +168,7 @@ namespace Squidex.Infrastructure.Queries
{ {
var json = new { path = "guid", op = "eq", value = 1 }; var json = new { path = "guid", op = "eq", value = 1 };
AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); AssertFilterError(json, "Expected String (Guid) for path 'guid', but got Number.");
} }
} }
@ -212,12 +206,28 @@ namespace Squidex.Infrastructure.Queries
AssertFilter(json, expected); AssertFilter(json, expected);
} }
[Fact]
public void Should_parse_filter_with_null()
{
var json = new { path = "stringNullable", op = "eq", value = (object?)null };
AssertFilter(json, "stringNullable == null");
}
[Fact]
public void Should_add_error_if_field_is_not_nullable()
{
var json = new { path = "string", op = "eq", value = (object?)null };
AssertFilterError(json, "Expected String for path 'string', but got Null.");
}
[Fact] [Fact]
public void Should_add_error_if_value_type_is_invalid() public void Should_add_error_if_value_type_is_invalid()
{ {
var json = new { path = "string", op = "eq", value = 1 }; var json = new { path = "string", op = "eq", value = 1 };
AssertErrors(json, "Expected String for path 'string', but got Number."); AssertFilterError(json, "Expected String for path 'string', but got Number.");
} }
[Fact] [Fact]
@ -225,7 +235,7 @@ namespace Squidex.Infrastructure.Queries
{ {
var json = new { path = "string", op = "matchs", value = "((" }; var json = new { path = "string", op = "matchs", value = "((" };
AssertErrors(json, "'((' is not a valid regular expression."); AssertFilterError(json, "'((' is not a valid regular expression at path 'string'.");
} }
[Fact] [Fact]
@ -235,14 +245,6 @@ namespace Squidex.Infrastructure.Queries
AssertFilter(json, "object.property in ['Hello']"); AssertFilter(json, "object.property in ['Hello']");
} }
[Fact]
public void Should_parse_referenced_string_filter()
{
var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } };
AssertFilter(json, "reference.property in ['Hello']");
}
} }
public class Geo public class Geo
@ -259,13 +261,6 @@ namespace Squidex.Infrastructure.Queries
return BuildFlatTests("geo", ValidOperator, value, $"Radius({value.longitude}, {value.latitude}, {value.distance})"); return BuildFlatTests("geo", ValidOperator, value, $"Radius({value.longitude}, {value.latitude}, {value.distance})");
} }
public static IEnumerable<object[]> ValidRefTests()
{
var value = new { longitude = 10, latitude = 20, distance = 30 };
return BuildFlatTests("geoRef", ValidOperator, value, $"Radius({value.longitude}, {value.latitude}, {value.distance})");
}
public static IEnumerable<object[]> InvalidTests() public static IEnumerable<object[]> InvalidTests()
{ {
var value = new { longitude = 10, latitude = 20, distance = 30 }; var value = new { longitude = 10, latitude = 20, distance = 30 };
@ -282,22 +277,13 @@ namespace Squidex.Infrastructure.Queries
AssertFilter(json, expected); AssertFilter(json, expected);
} }
[Theory]
[MemberData(nameof(ValidRefTests))]
public void Should_parse_filter_with_reference(string field, string op, object value, string expected)
{
var json = new { path = field, op, value };
AssertFilter(json, expected);
}
[Theory] [Theory]
[MemberData(nameof(InvalidTests))] [MemberData(nameof(InvalidTests))]
public void Should_add_error_if_operator_is_invalid(string field, string op, object value, string expected) public void Should_add_error_if_operator_is_invalid(string field, string op, object value, string expected)
{ {
var json = new { path = field, op, value }; var json = new { path = field, op, value };
AssertErrors(json, $"'{expected}' is not a valid operator for type Object(geo-json) at '{field}'."); AssertFilterError(json, $"'{expected}' is not a valid operator for type GeoObject at '{field}'.");
} }
[Fact] [Fact]
@ -305,7 +291,7 @@ namespace Squidex.Infrastructure.Queries
{ {
var json = new { path = "geo", op = "lt", value = new { latitude = 10, longitude = 20 } }; var json = new { path = "geo", op = "lt", value = new { latitude = 10, longitude = 20 } };
AssertErrors(json, "Expected Object(geo-json) for path 'geo', but got Object."); AssertFilterError(json, "Expected Object(geo-json) for path 'geo', but got Object.");
} }
[Fact] [Fact]
@ -313,7 +299,7 @@ namespace Squidex.Infrastructure.Queries
{ {
var json = new { path = "geo", op = "lt", value = 1 }; var json = new { path = "geo", op = "lt", value = 1 };
AssertErrors(json, "Expected Object(geo-json) for path 'geo', but got Number."); AssertFilterError(json, "Expected Object(geo-json) for path 'geo', but got Number.");
} }
} }
@ -360,7 +346,7 @@ namespace Squidex.Infrastructure.Queries
{ {
var json = new { path = field, op, value }; var json = new { path = field, op, value };
AssertErrors(json, $"'{expected}' is not a valid operator for type Number at '{field}'."); AssertFilterError(json, $"'{expected}' is not a valid operator for type Number at '{field}'.");
} }
[Theory] [Theory]
@ -372,12 +358,28 @@ namespace Squidex.Infrastructure.Queries
AssertFilter(json, expected); AssertFilter(json, expected);
} }
[Fact]
public void Should_parse_filter_with_null()
{
var json = new { path = "numberNullable", op = "eq", value = (object?)null };
AssertFilter(json, "numberNullable == null");
}
[Fact]
public void Should_add_error_if_field_is_not_nullable()
{
var json = new { path = "number", op = "eq", value = (object?)null };
AssertFilterError(json, "Expected Number for path 'number', but got Null.");
}
[Fact] [Fact]
public void Should_add_error_if_value_type_is_invalid() public void Should_add_error_if_value_type_is_invalid()
{ {
var json = new { path = "number", op = "eq", value = true }; var json = new { path = "number", op = "eq", value = true };
AssertErrors(json, "Expected Number for path 'number', but got Boolean."); AssertFilterError(json, "Expected Number for path 'number', but got Boolean.");
} }
} }
@ -424,7 +426,7 @@ namespace Squidex.Infrastructure.Queries
{ {
var json = new { path = field, op, value }; var json = new { path = field, op, value };
AssertErrors(json, $"'{expected}' is not a valid operator for type Boolean at '{field}'."); AssertFilterError(json, $"'{expected}' is not a valid operator for type Boolean at '{field}'.");
} }
[Theory] [Theory]
@ -436,12 +438,28 @@ namespace Squidex.Infrastructure.Queries
AssertFilter(json, expected); AssertFilter(json, expected);
} }
[Fact]
public void Should_parse_filter_with_null()
{
var json = new { path = "booleanNullable", op = "eq", value = (object?)null };
AssertFilter(json, "booleanNullable == null");
}
[Fact]
public void Should_add_error_if_field_is_not_nullable()
{
var json = new { path = "boolean", op = "eq", value = (object?)null };
AssertFilterError(json, "Expected Boolean for path 'boolean', but got Null.");
}
[Fact] [Fact]
public void Should_add_error_if_boolean_property_got_invalid_value() public void Should_add_error_if_boolean_property_got_invalid_value()
{ {
var json = new { path = "boolean", op = "eq", value = 1 }; var json = new { path = "boolean", op = "eq", value = 1 };
AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); AssertFilterError(json, "Expected Boolean for path 'boolean', but got Number.");
} }
} }
@ -484,12 +502,28 @@ namespace Squidex.Infrastructure.Queries
AssertFilter(json, expected); AssertFilter(json, expected);
} }
[Fact]
public void Should_parse_filter_with_null()
{
var json = new { path = "stringArrayNullable", op = "eq", value = (object?)null };
AssertFilter(json, "stringArrayNullable == null");
}
[Fact]
public void Should_add_error_if_field_is_not_nullable()
{
var json = new { path = "stringArray", op = "eq", value = (object?)null };
AssertFilterError(json, "Expected String for path 'stringArray', but got Null.");
}
[Fact] [Fact]
public void Should_add_error_if_using_array_value_for_non_allowed_operator() public void Should_add_error_if_using_array_value_for_non_allowed_operator()
{ {
var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; var json = new { path = "string", op = "eq", value = new[] { "Hello" } };
AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); AssertFilterError(json, "Array value is not allowed for 'Equals' operator and path 'string'.");
} }
[Fact] [Fact]
@ -502,27 +536,43 @@ namespace Squidex.Infrastructure.Queries
} }
[Fact] [Fact]
public void Should_add_error_if_property_does_not_exist() public void Should_filter_union_by_string()
{ {
var json = new { path = "notfound", op = "eq", value = 1 }; var json = new { path = "union", op = "eq", value = "Hello" };
AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); AssertFilter(json, "union == 'Hello'");
} }
[Fact] [Fact]
public void Should_add_error_if_nested_property_does_not_exist() public void Should_filter_union_by_number()
{ {
var json = new { path = "object.notfound", op = "eq", value = 1 }; var json = new { path = "union", op = "eq", value = 42 };
AssertFilter(json, "union == 42");
}
[Fact]
public void Should_not_filter_union_by_boolean()
{
var json = new { path = "union", op = "eq", value = true };
AssertErrors(json, "'notfound' is not a property of 'nested'."); AssertFilterError(json, "Expected String for path 'union', but got Boolean.");
} }
[Fact] [Fact]
public void Should_add_error_if_nested_reference_property_does_not_exist() public void Should_add_error_if_property_does_not_exist()
{ {
var json = new { path = "reference.notfound", op = "eq", value = 1 }; var json = new { path = "notfound", op = "eq", value = 1 };
AssertFilterError(json, "Path 'notfound' does not point to a valid property in the model.");
}
[Fact]
public void Should_add_error_if_nested_property_does_not_exist()
{
var json = new { path = "object.notfound", op = "eq", value = 1 };
AssertErrors(json, "'notfound' is not a property of 'nested'."); AssertFilterError(json, "Path 'object.notfound' does not point to a valid property in the model.");
} }
[Fact] [Fact]
@ -585,7 +635,7 @@ namespace Squidex.Infrastructure.Queries
Assert.Equal(expectedFilter, filter); Assert.Equal(expectedFilter, filter);
} }
private static void AssertErrors(object json, string expectedError) private static void AssertFilterError(object json, string expectedError)
{ {
var errors = new List<string>(); var errors = new List<string>();
@ -601,14 +651,14 @@ namespace Squidex.Infrastructure.Queries
var jsonFilter = TestUtils.DefaultSerializer.Deserialize<FilterNode<IJsonValue>>(json); var jsonFilter = TestUtils.DefaultSerializer.Deserialize<FilterNode<IJsonValue>>(json);
return JsonFilterVisitor.Parse(jsonFilter, Schema, errors)?.ToString(); return JsonFilterVisitor.Parse(jsonFilter, Model, errors)?.ToString();
} }
private static string? ConvertQuery<T>(T value) private static string? ConvertQuery<T>(T value)
{ {
var json = TestUtils.DefaultSerializer.Serialize(value, true); var json = TestUtils.DefaultSerializer.Serialize(value, true);
var jsonFilter = Schema.Parse(json, TestUtils.DefaultSerializer); var jsonFilter = Model.Parse(json, TestUtils.DefaultSerializer);
return jsonFilter.ToString(); return jsonFilter.ToString();
} }

141
backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Microsoft.OData.Edm; using Microsoft.OData.Edm;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Queries.OData; using Squidex.Infrastructure.Queries.OData;
using Xunit; using Xunit;
@ -20,39 +21,35 @@ namespace Squidex.Infrastructure.Queries
static QueryFromODataTests() static QueryFromODataTests()
{ {
var entityType = new EdmEntityType("Squidex", "Users"); var fields = new List<FilterField>
{
entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.Guid, false); new FilterField(FilterSchema.Guid, "id"),
entityType.AddStructuralProperty("idNullable", EdmPrimitiveTypeKind.Guid, true); new FilterField(FilterSchema.Guid, "idNullable", IsNullable: true),
entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset, false); new FilterField(FilterSchema.DateTime, "created"),
entityType.AddStructuralProperty("createdNullable", EdmPrimitiveTypeKind.DateTimeOffset, true); new FilterField(FilterSchema.DateTime, "createdNullable", IsNullable: true),
entityType.AddStructuralProperty("isComicFigure", EdmPrimitiveTypeKind.Boolean, false); new FilterField(FilterSchema.Boolean, "isComicFigure"),
entityType.AddStructuralProperty("isComicFigureNullable", EdmPrimitiveTypeKind.Boolean, true); new FilterField(FilterSchema.Boolean, "isComicFigureNullable", IsNullable: true),
entityType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String, true); new FilterField(FilterSchema.String, "firstName"),
entityType.AddStructuralProperty("firstNameNullable", EdmPrimitiveTypeKind.String, false); new FilterField(FilterSchema.String, "firstNameNullable", IsNullable: true),
entityType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String, true); new FilterField(FilterSchema.String, "lastName"),
entityType.AddStructuralProperty("birthday", EdmPrimitiveTypeKind.Date, false); new FilterField(FilterSchema.String, "lastNameNullable", IsNullable: true),
entityType.AddStructuralProperty("birthdayNullable", EdmPrimitiveTypeKind.Date, true); new FilterField(FilterSchema.Number, "age"),
entityType.AddStructuralProperty("incomeCents", EdmPrimitiveTypeKind.Int64, false); new FilterField(FilterSchema.Number, "ageNullable", IsNullable: true),
entityType.AddStructuralProperty("incomeCentsNullable", EdmPrimitiveTypeKind.Int64, true); new FilterField(FilterSchema.Number, "incomeMio"),
entityType.AddStructuralProperty("incomeMio", EdmPrimitiveTypeKind.Double, false); new FilterField(FilterSchema.Number, "incomeMioNullable", IsNullable: true),
entityType.AddStructuralProperty("incomeMioNullable", EdmPrimitiveTypeKind.Double, true); new FilterField(FilterSchema.GeoObject, "geo"),
entityType.AddStructuralProperty("geo", EdmPrimitiveTypeKind.GeographyPoint, false); new FilterField(FilterSchema.GeoObject, "geoNullable", IsNullable: true),
entityType.AddStructuralProperty("geoNullable", EdmPrimitiveTypeKind.GeographyPoint, true); new FilterField(FilterSchema.Any, "properties"),
entityType.AddStructuralProperty("age", EdmPrimitiveTypeKind.Int32, false); };
entityType.AddStructuralProperty("ageNullable", EdmPrimitiveTypeKind.Int32, true);
entityType.AddStructuralProperty("properties", new EdmComplexTypeReference(new EdmComplexType("Squidex", "Properties", null, false, true), true)); var filterSchema = new FilterSchema(FilterSchemaType.Object)
{
var container = new EdmEntityContainer("Squidex", "Container"); Fields = fields.ToReadonlyList()
};
container.AddEntitySet("UserSet", entityType);
var queryModel = new QueryModel { Schema = filterSchema };
var model = new EdmModel();
EdmModel = queryModel.ConvertToEdm("Squidex", "Content");
model.AddElement(container);
model.AddElement(entityType);
EdmModel = model;
} }
[Fact] [Fact]
@ -63,6 +60,18 @@ namespace Squidex.Infrastructure.Queries
Assert.NotNull(parser); Assert.NotNull(parser);
} }
[Fact]
public void Should_escape_field_name()
{
Assert.Equal("field_name", "field-name".EscapeEdmField());
}
[Fact]
public void Should_unescape_field_name()
{
Assert.Equal("field-name", "field_name".UnescapeEdmField());
}
[Theory] [Theory]
[InlineData("created")] [InlineData("created")]
[InlineData("createdNullable")] [InlineData("createdNullable")]
@ -107,28 +116,6 @@ namespace Squidex.Infrastructure.Queries
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Theory]
[InlineData("birthday")]
[InlineData("birthdayNullable")]
[InlineData("properties/date")]
[InlineData("properties/nested/date")]
public void Should_parse_filter_if_type_is_date(string field)
{
var i = _Q($"$filter={field} eq 1988-01-19");
var o = _C($"Filter: {field} == 1988-01-19T00:00:00Z");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_if_type_is_date_list()
{
var i = _Q("$filter=birthday in ('1988-01-19')");
var o = _C("Filter: birthday in [1988-01-19T00:00:00Z]");
Assert.Equal(o, i);
}
[Theory] [Theory]
[InlineData("id")] [InlineData("id")]
[InlineData("idNullable")] [InlineData("idNullable")]
@ -230,50 +217,6 @@ namespace Squidex.Infrastructure.Queries
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Theory]
[InlineData("age")]
[InlineData("ageNullable")]
[InlineData("properties/int")]
[InlineData("properties/nested/int")]
public void Should_parse_filter_if_type_is_int32(string field)
{
var i = _Q($"$filter={field} eq 60");
var o = _C($"Filter: {field} == 60");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_if_type_is_int32_list()
{
var i = _Q("$filter=age in (60)");
var o = _C("Filter: age in [60]");
Assert.Equal(o, i);
}
[Theory]
[InlineData("incomeCents")]
[InlineData("incomeCentsNullable")]
[InlineData("properties/long")]
[InlineData("properties/nested/long")]
public void Should_parse_filter_if_type_is_int64(string field)
{
var i = _Q($"$filter={field} eq 31543143513456789");
var o = _C($"Filter: {field} == 31543143513456789");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_if_type_is_int64_list()
{
var i = _Q("$filter=incomeCents in (31543143513456789)");
var o = _C("Filter: incomeCents in [31543143513456789]");
Assert.Equal(o, i);
}
[Theory] [Theory]
[InlineData("incomeMio")] [InlineData("incomeMio")]
[InlineData("incomeMioNullable")] [InlineData("incomeMioNullable")]

8
backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs

@ -157,14 +157,6 @@ namespace Squidex.Infrastructure.Queries
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json));
} }
[Fact]
public void Should_throw_exception_for_invalid_property()
{
var json = new { path = "property", op = "invalid", value = 12, other = 4 };
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json));
}
[Fact] [Fact]
public void Should_not_throw_exception_if_filter_has_unknown_property() public void Should_not_throw_exception_if_filter_has_unknown_property()
{ {

8
backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestUtils.cs

@ -14,6 +14,7 @@ using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Newtonsoft; using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.Json;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Infrastructure.TestHelpers namespace Squidex.Infrastructure.TestHelpers
@ -36,11 +37,12 @@ namespace Squidex.Infrastructure.TestHelpers
SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()), SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()),
ContractResolver = new ConverterContractResolver( ContractResolver = new ConverterContractResolver(
new SurrogateConverter<ClaimsPrincipal, ClaimsPrincipalSurrogate>(), new CompareOperatorJsonConverter(),
new EnvelopeHeadersConverter(),
new JsonValueConverter(), new JsonValueConverter(),
new StringEnumConverter(),
new SurrogateConverter<ClaimsPrincipal, ClaimsPrincipalSurrogate>(),
new SurrogateConverter<FilterNode<IJsonValue>, JsonFilterSurrogate>(), new SurrogateConverter<FilterNode<IJsonValue>, JsonFilterSurrogate>(),
new StringEnumConverter()), new EnvelopeHeadersConverter()),
TypeNameHandling = TypeNameHandling.Auto TypeNameHandling = TypeNameHandling.Auto
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);

6
frontend/src/app/features/assets/pages/assets-page.component.html

@ -20,11 +20,11 @@
</div> </div>
<div class="col-6"> <div class="col-6">
<sqx-search-form formClass="form" placeholder="{{ 'assets.searchByName' | sqxTranslate }}" fieldExample="fileSize" <sqx-search-form formClass="form" placeholder="{{ 'assets.searchByName' | sqxTranslate }}" fieldExample="fileSize"
[query]="assetsState.query | async" [enableShortcut]="true"
[queries]="queries" [queries]="queries"
[queriesTypes]="'common.assets' | sqxTranslate" [queriesTypes]="'common.assets' | sqxTranslate"
(queryChange)="search($event)" [query]="assetsState.query | async"
[enableShortcut]="true"> (queryChange)="search($event)">
</sqx-search-form> </sqx-search-form>
</div> </div>
</div> </div>

5
frontend/src/app/features/content/pages/contents/contents-page.component.html

@ -12,14 +12,15 @@
</div> </div>
<div class="col"> <div class="col">
<sqx-search-form formClass="form" placeholder="{{ 'contents.searchPlaceholder' | sqxTranslate }}" <sqx-search-form formClass="form" placeholder="{{ 'contents.searchPlaceholder' | sqxTranslate }}"
(queryChange)="search($event)"
[enableShortcut]="true" [enableShortcut]="true"
[language]="(languagesState.isoMasterLanguage | async)!" [language]="(languagesState.isoMasterLanguage | async)!"
[languages]="languages" [languages]="languages"
[queries]="queries | async" [queries]="queries | async"
[queriesTypes]="'common.contents' | sqxTranslate" [queriesTypes]="'common.contents' | sqxTranslate"
(queryChange)="search($event)"
[query]="contentsState.query | async" [query]="contentsState.query | async"
[queryModel]="queryModel | async"> [queryModel]="queryModel | async"
[statuses]="contentsState.statuses | async">
</sqx-search-form> </sqx-search-form>
</div> </div>
<div class="col-auto" *ngIf="languages.length > 1"> <div class="col-auto" *ngIf="languages.length > 1">

16
frontend/src/app/features/content/pages/contents/contents-page.component.ts

@ -9,9 +9,8 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest } from 'rxjs';
import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators';
import { AppLanguageDto, AppsState, ContentDto, ContentsState, ContributorsState, defined, LanguagesState, ModalModel, Queries, Query, queryModelFromSchema, QuerySynchronizer, ResourceOwner, Router2State, SchemaDto, SchemasState, switchSafe, TableFields, TempService, UIState } from '@app/shared'; import { AppLanguageDto, AppsState, ContentDto, ContentsState, ContributorsState, defined, LanguagesState, ModalModel, Queries, Query, QuerySynchronizer, ResourceOwner, Router2State, SchemaDto, SchemasService, SchemasState, switchSafe, TableFields, TempService, UIState } from '@app/shared';
import { DueTimeSelectorComponent } from './../../shared/due-time-selector.component'; import { DueTimeSelectorComponent } from './../../shared/due-time-selector.component';
@Component({ @Component({
@ -47,16 +46,12 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
} }
public queryModel = public queryModel =
combineLatest([ this.schemasState.selectedSchema.pipe(defined(), map(x => x.name), distinctUntilChanged(),
this.schemasState.selectedSchema.pipe(defined()), switchMap(x => this.schemasService.getFilters(this.appsState.appName, x)));
this.languagesState.isoLanguages,
this.contentsState.statuses,
]).pipe(
map(values => queryModelFromSchema(values[0], values[1], values[2])));
public queries = public queries =
this.schemasState.selectedSchema.pipe(defined(), this.schemasState.selectedSchema.pipe(defined(), map(x => x.name), distinctUntilChanged(),
map(schema => new Queries(this.uiState, `schemas.${schema.name}`))); map(x => new Queries(this.uiState, `schemas.${x}`)));
constructor( constructor(
public readonly contentsRoute: Router2State, public readonly contentsRoute: Router2State,
@ -67,6 +62,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router, private readonly router: Router,
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
private readonly schemasService: SchemasService,
private readonly tempService: TempService, private readonly tempService: TempService,
private readonly uiState: UIState, private readonly uiState: UIState,
) { ) {

4
frontend/src/app/framework/angular/forms/editors/autocomplete.component.html

@ -22,10 +22,10 @@
<ng-container *sqxModal="suggestionsModal"> <ng-container *sqxModal="suggestionsModal">
<sqx-dropdown-menu class="control-dropdown" #container <sqx-dropdown-menu class="control-dropdown" #container
[sqxAnchoredTo]="input" [sqxAnchoredTo]="input"
[adjustWidth]="true" [adjustWidth]="dropdownFullWidth"
[adjustHeight]="false" [adjustHeight]="false"
[scrollY]="true" [scrollY]="true"
[style.minWidth]="dropdownWidth" [style]="dropdownStyles"
[position]="dropdownPosition"> [position]="dropdownPosition">
<div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable" <div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex" [class.active]="i === snapshot.suggestedIndex"

5
frontend/src/app/framework/angular/forms/editors/autocomplete.component.ts

@ -82,7 +82,10 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
public dropdownPosition: RelativePosition = 'bottom-left'; public dropdownPosition: RelativePosition = 'bottom-left';
@Input() @Input()
public dropdownWidth = '18rem'; public dropdownFullWidth = true;
@Input()
public dropdownStyles: any = {};
@Input() @Input()
public set disabled(value: boolean | undefined | null) { public set disabled(value: boolean | undefined | null) {

8
frontend/src/app/framework/angular/forms/editors/dropdown.component.html

@ -17,10 +17,12 @@
<ng-container *sqxModal="dropdown"> <ng-container *sqxModal="dropdown">
<sqx-dropdown-menu <sqx-dropdown-menu
[sqxAnchoredTo]="input" [sqxAnchoredTo]="input"
[adjustWidth]="true" [adjustWidth]="dropdownFullWidth"
[adjustHeight]="false" [adjustHeight]="false"
[scrollX]="false"
[scrollY]="true" [scrollY]="true"
position="bottom-left"> [style]="dropdownStyles"
[position]="dropdownPosition">
<div *ngIf="canSearch" class="search-form"> <div *ngIf="canSearch" class="search-form">
<input class="form-control search" [formControl]="queryInput" placeholder="{{ 'contributors.search' | sqxTranslate }}" (keydown)="onKeyDown($event)" sqxFocusOnInit> <input class="form-control search" [formControl]="queryInput" placeholder="{{ 'contributors.search' | sqxTranslate }}" (keydown)="onKeyDown($event)" sqxFocusOnInit>
</div> </div>
@ -28,7 +30,7 @@
<div class="control-dropdown-items" #container> <div class="control-dropdown-items" #container>
<div *ngFor="let item of snapshot.suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable" <div *ngFor="let item of snapshot.suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex" [class.active]="i === snapshot.suggestedIndex"
[class.separated]="itemsBordered" [class.separated]="itemSeparator"
(mousedown)="selectIndexAndClose(i)" (mousedown)="selectIndexAndClose(i)"
[sqxScrollActive]="i === snapshot.suggestedIndex" [sqxScrollActive]="i === snapshot.suggestedIndex"
[sqxScrollContainer]="container"> [sqxScrollContainer]="container">

13
frontend/src/app/framework/angular/forms/editors/dropdown.component.ts

@ -8,7 +8,7 @@
import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, EventEmitter, forwardRef, Input, OnChanges, OnInit, Output, QueryList, SimpleChanges, TemplateRef } from '@angular/core'; import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, EventEmitter, forwardRef, Input, OnChanges, OnInit, Output, QueryList, SimpleChanges, TemplateRef } from '@angular/core';
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { Keys, ModalModel, StatefulControlComponent, Types } from '@app/framework/internal'; import { Keys, ModalModel, RelativePosition, StatefulControlComponent, Types } from '@app/framework/internal';
export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true,
@ -50,7 +50,7 @@ export class DropdownComponent extends StatefulControlComponent<State, ReadonlyA
public items: ReadonlyArray<any> | undefined | null = []; public items: ReadonlyArray<any> | undefined | null = [];
@Input() @Input()
public itemsBordered?: boolean | null; public itemSeparator?: boolean | null;
@Input() @Input()
public searchProperty = 'name'; public searchProperty = 'name';
@ -61,6 +61,15 @@ export class DropdownComponent extends StatefulControlComponent<State, ReadonlyA
@Input() @Input()
public canSearch?: boolean | null = true; public canSearch?: boolean | null = true;
@Input()
public dropdownPosition: RelativePosition = 'bottom-left';
@Input()
public dropdownFullWidth = false;
@Input()
public dropdownStyles: any = {};
@Input() @Input()
public set disabled(value: boolean | undefined | null) { public set disabled(value: boolean | undefined | null) {
this.setDisabledState(value === true); this.setDisabledState(value === true);

2
frontend/src/app/framework/angular/forms/editors/tag-editor.component.html

@ -39,7 +39,7 @@
position="bottom-left" #container> position="bottom-left" #container>
<div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable" <div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex" [class.active]="i === snapshot.suggestedIndex"
[class.separated]="separated" [class.separated]="itemSeparator"
(mousedown)="selectValue(item)" (mousedown)="selectValue(item)"
(mouseover)="selectIndex(i)" (mouseover)="selectIndex(i)"
[sqxScrollActive]="i === snapshot.suggestedIndex" [sqxScrollActive]="i === snapshot.suggestedIndex"

2
frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts

@ -77,7 +77,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
public dashed?: boolean | null; public dashed?: boolean | null;
@Input() @Input()
public separated?: boolean | null; public itemSeparator?: boolean | null;
@Input() @Input()
public singleLine?: boolean | null; public singleLine?: boolean | null;

2
frontend/src/app/shared/components/assets/assets-selector.component.html

@ -21,8 +21,8 @@
</div> </div>
<div class="col-6"> <div class="col-6">
<sqx-search-form formClass="form" placeholder="{{ 'assets.searchByName' | sqxTranslate }}" <sqx-search-form formClass="form" placeholder="{{ 'assets.searchByName' | sqxTranslate }}"
(queryChange)="search($event)"
[enableShortcut]="true"> [enableShortcut]="true">
(queryChange)="search($event)"
[query]="assetsState.query | async" [query]="assetsState.query | async"
</sqx-search-form> </sqx-search-form>
</div> </div>

3
frontend/src/app/shared/components/references/content-selector.component.html

@ -25,7 +25,8 @@
[languages]="languages" [languages]="languages"
(queryChange)="search($event)" (queryChange)="search($event)"
[query]="contentsState.query | async" [query]="contentsState.query | async"
[queryModel]="queryModel"> [queryModel]="queryModel | async"
[statuses]="contentsState.statuses | async">
</sqx-search-form> </sqx-search-form>
</div> </div>

22
frontend/src/app/shared/components/references/content-selector.component.ts

@ -6,7 +6,9 @@
*/ */
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ApiUrlConfig, AppsState, ComponentContentsState, ContentDto, LanguageDto, Query, QueryModel, queryModelFromSchema, ResourceOwner, SchemaDto, SchemasState } from '@app/shared/internal'; import { BehaviorSubject, of } from 'rxjs';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
import { ApiUrlConfig, AppsState, ComponentContentsState, ContentDto, LanguageDto, Query, ResourceOwner, SchemaDto, SchemasService, SchemasState } from '@app/shared/internal';
@Component({ @Component({
selector: 'sqx-content-selector[language][languages]', selector: 'sqx-content-selector[language][languages]',
@ -44,17 +46,27 @@ export class ContentSelectorComponent extends ResourceOwner implements OnInit {
public schema!: SchemaDto; public schema!: SchemaDto;
public schemas: ReadonlyArray<SchemaDto> = []; public schemas: ReadonlyArray<SchemaDto> = [];
public queryModel!: QueryModel;
public selectedItems: { [id: string]: ContentDto } = {}; public selectedItems: { [id: string]: ContentDto } = {};
public selectionCount = 0; public selectionCount = 0;
public selectedAll = false; public selectedAll = false;
public querySource = new BehaviorSubject<SchemaDto | null>(null);
public queryModel =
this.querySource.pipe(map(x => x?.name), distinctUntilChanged(),
switchMap(x => {
if (x) {
return this.schemasService.getFilters(this.appsState.appName, x);
} else {
return of(null);
}
}));
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,
public readonly apiUrl: ApiUrlConfig, public readonly apiUrl: ApiUrlConfig,
public readonly contentsState: ComponentContentsState, public readonly contentsState: ComponentContentsState,
public readonly schemasState: SchemasState, public readonly schemasState: SchemasState,
public readonly schemasService: SchemasService,
) { ) {
super(); super();
} }
@ -140,9 +152,7 @@ export class ContentSelectorComponent extends ResourceOwner implements OnInit {
} }
private updateModel() { private updateModel() {
if (this.schema) { this.querySource.next(this.schema);
this.queryModel = queryModelFromSchema(this.schema, this.languages, this.contentsState.snapshot.statuses);
}
} }
public trackByContent(_index: number, content: ContentDto): string { public trackByContent(_index: number, content: ContentDto): string {

63
frontend/src/app/shared/components/search/queries/filter-comparison.component.html

@ -1,4 +1,4 @@
<div class="row gx-2 mb-1 align-items-center" *ngIf="fieldModel"> <div class="row gx-2 mb-1 align-items-center" *ngIf="field">
<div class="col-auto path"> <div class="col-auto path">
<sqx-query-path <sqx-query-path
(pathChange)="changePath($event)" (pathChange)="changePath($event)"
@ -7,50 +7,42 @@
</sqx-query-path> </sqx-query-path>
</div> </div>
<div class="col-auto operator"> <div class="col-auto operator">
<select class="form-select" [ngModel]="filter.op" (ngModelChange)="changeOp($event)"> <select class="form-select"
<option *ngFor="let operator of fieldModel.operators" [ngValue]="operator.value">{{operator.name | sqxTranslate}}</option> [disabled]="operators.length === 0"
[ngModel]="filter.op"
(ngModelChange)="changeOp($event)">
<option *ngFor="let operator of operators" [ngValue]="operator">{{operator | sqxFilterOperator | sqxTranslate}}</option>
</select> </select>
</div> </div>
<div class="col" *ngIf="!noValue" [ngSwitch]="fieldModel.type"> <div class="col align-items-center" [ngSwitch]="fieldUI">
<ng-container *ngSwitchCase="'boolean'"> <ng-container *ngSwitchCase="'Boolean'">
<div class="form-check form-check-inline ps-2"> <input type="checkbox" class="form-check-input"
<input type="checkbox" class="form-check-input" [ngModel]="filter.value"
[ngModel]="filter.value" (ngModelChange)="changeValue($event)" />
(ngModelChange)="changeValue($event)" />
</div>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'date'"> <ng-container *ngSwitchCase="'Date'">
<sqx-date-time-editor mode="Date" <sqx-date-time-editor mode="Date"
[hideDateButtons]="true" [hideDateButtons]="true"
[ngModel]="filter.value" [ngModel]="filter.value"
(ngModelChange)="changeValue($event)"> (ngModelChange)="changeValue($event)">
</sqx-date-time-editor> </sqx-date-time-editor>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'datetime'"> <ng-container *ngSwitchCase="'DateTime'">
<sqx-date-time-editor mode="DateTime" <sqx-date-time-editor mode="DateTime"
[hideDateButtons]="true" [hideDateButtons]="true"
[ngModel]="filter.value" [ngModel]="filter.value"
(ngModelChange)="changeValue($event)"> (ngModelChange)="changeValue($event)">
</sqx-date-time-editor> </sqx-date-time-editor>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'number'"> <ng-container *ngSwitchCase="'Number'">
<input type="number" class="form-control" <input type="number" class="form-control"
[ngModel]="filter.value" [ngModel]="filter.value"
(ngModelChange)="changeValue($event)" /> (ngModelChange)="changeValue($event)" />
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'reference'"> <ng-container *ngSwitchCase="'Status'">
<sqx-reference-input [schemaIds]="fieldModel.extra"
mode="Single"
[ngModel]="filter.value"
(ngModelChange)="changeValue($event)"
[language]="language"
[languages]="languages">
</sqx-reference-input>
</ng-container>
<ng-container *ngSwitchCase="'status'">
<sqx-dropdown <sqx-dropdown
valueProperty="status" valueProperty="status"
[items]="fieldModel.extra" [items]="statuses"
[ngModel]="filter.value" [ngModel]="filter.value"
(ngModelChange)="changeValue($event)" (ngModelChange)="changeValue($event)"
[canSearch]="false"> [canSearch]="false">
@ -59,7 +51,7 @@
</ng-template> </ng-template>
</sqx-dropdown> </sqx-dropdown>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'user'"> <ng-container *ngSwitchCase="'User'">
<ng-container *ngIf="contributorsState.isLoaded | async; else noPermission"> <ng-container *ngIf="contributorsState.isLoaded | async; else noPermission">
<sqx-dropdown <sqx-dropdown
valueProperty="token" valueProperty="token"
@ -80,17 +72,28 @@
</sqx-dropdown> </sqx-dropdown>
</ng-container> </ng-container>
<ng-template #noPermission> <ng-template #noPermission>
<input type="text" class="form-control" *ngIf="!fieldModel.extra" <input type="text" class="form-control" *ngIf="!field.schema.extra"
[ngModel]="filter.value" [ngModel]="filter.value"
(ngModelChange)="changeValue($event)" (ngModelChange)="changeValue($event)" />
/>
</ng-template> </ng-template>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'string'"> <ng-container *ngSwitchCase="'String'">
<input type="text" class="form-control" *ngIf="!fieldModel.extra" <input type="text" class="form-control" *ngIf="!field.schema.extra"
[ngModel]="filter.value" [ngModel]="filter.value"
(ngModelChange)="changeValue($event)" /> (ngModelChange)="changeValue($event)" />
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Reference'">
<sqx-reference-input [schemaIds]="field.schema.extra?.schemaIds"
mode="Single"
[ngModel]="filter.value"
(ngModelChange)="changeValue($event)"
[language]="language"
[languages]="languages">
</sqx-reference-input>
</ng-container>
<ng-container *ngSwitchCase="'Unsupported'">
{{ 'common.notSupported' | sqxTranslate }}
</ng-container>
</div> </div>
<div class="col-auto ps-2"> <div class="col-auto ps-2">
<button type="button" class="btn btn-text-danger" (click)="remove.emit()"> <button type="button" class="btn btn-text-danger" (click)="remove.emit()">

46
frontend/src/app/shared/components/search/queries/filter-comparison.component.ts

@ -5,12 +5,12 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { FilterComparison, LanguageDto, QueryFieldModel, QueryModel } from '@app/shared/internal'; import { FilterComparison, LanguageDto, FilterableField, QueryModel, FilterFieldUI, getFilterUI, StatusInfo } from '@app/shared/internal';
import { ContributorsState } from '@app/shared/state/contributors.state'; import { ContributorsState } from '@app/shared/state/contributors.state';
@Component({ @Component({
selector: 'sqx-filter-comparison[filter][language][languages][model]', selector: 'sqx-filter-comparison[filter][language][languages][model][statuses]',
styleUrls: ['./filter-comparison.component.scss'], styleUrls: ['./filter-comparison.component.scss'],
templateUrl: './filter-comparison.component.html', templateUrl: './filter-comparison.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -28,26 +28,26 @@ export class FilterComparisonComponent implements OnChanges {
@Input() @Input()
public languages!: ReadonlyArray<LanguageDto>; public languages!: ReadonlyArray<LanguageDto>;
@Input()
public statuses?: ReadonlyArray<StatusInfo> | null;
@Input() @Input()
public model!: QueryModel; public model!: QueryModel;
@Input() @Input()
public filter!: FilterComparison; public filter!: FilterComparison;
public fieldModel?: QueryFieldModel; public field?: FilterableField;
public fieldUI?: FilterFieldUI;
public noValue = false; public operators: ReadonlyArray<string> = [];
constructor( constructor(
public readonly contributorsState: ContributorsState, public readonly contributorsState: ContributorsState,
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges() {
if (changes['filter']) { this.updatePath(false);
this.updatePath(false);
this.updateOperator();
}
} }
public changeValue(value: any) { public changeValue(value: any) {
@ -59,7 +59,7 @@ export class FilterComparisonComponent implements OnChanges {
public changeOp(op: string) { public changeOp(op: string) {
this.filter.op = op; this.filter.op = op;
this.updateOperator(); this.updatePath(false);
this.emitChange(); this.emitChange();
} }
@ -72,26 +72,20 @@ export class FilterComparisonComponent implements OnChanges {
this.emitChange(); this.emitChange();
} }
private updateOperator() { private updatePath(updateValue: boolean) {
if (this.fieldModel) { this.field = this.model.schema.fields.find(x => x.path === this.filter.path);
const operator = this.fieldModel.operators.find(x => x.value === this.filter.op);
this.noValue = !!(operator && operator.noValue); this.operators = this.model.operators[this.field?.schema.type!] || [];
}
}
private updatePath(refresh: boolean) { if (this.operators.indexOf(this.filter.op) < 0) {
const newModel = this.model.fields[this.filter.path]; this.filter.op = this.operators[0];
}
if (newModel && refresh) {
if (!newModel.operators.find(x => x.value === this.filter.op)) {
this.filter.op = newModel.operators[0].value;
}
if (updateValue) {
this.filter.value = null; this.filter.value = null;
} }
this.fieldModel = newModel; this.fieldUI = getFilterUI(this.filter, this.field!);
} }
public emitChange() { public emitChange() {

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save