Browse Source

Merge branch 'master' of github.com:Squidex/squidex

pull/468/head
Sebastian 6 years ago
parent
commit
cb23a8ad5a
  1. 1
      backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs
  2. 1
      backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs
  3. 10
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  4. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs
  5. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs
  6. 17
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
  7. 11
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  8. 110
      backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs
  9. 11
      backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs
  10. 10
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs
  11. 31
      backend/src/Squidex.Domain.Apps.Core.Model/DeepComparer.cs
  12. 48
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs
  13. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
  14. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs
  15. 5
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
  16. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs
  17. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
  18. 25
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs
  19. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs
  20. 25
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs
  21. 5
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs
  22. 70
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
  23. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs
  24. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs
  25. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  26. 15
      backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  27. 9
      backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs
  28. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs
  29. 31
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs
  30. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs
  31. 19
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  32. 11
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs
  33. 13
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs
  34. 19
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  35. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
  36. 30
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathConverter.cs
  37. 25
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathExtension.cs
  38. 6
      backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs
  39. 9
      backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  40. 209
      backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  41. 36
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  42. 9
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs
  43. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs
  44. 10
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  45. 3
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs
  46. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  47. 5
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs
  48. 222
      backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs
  49. 19
      backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs
  50. 19
      backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs
  51. 12
      backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs
  52. 20
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs
  53. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs
  54. 69
      backend/src/Squidex.Domain.Apps.Entities/Assets/ImageMetadataSource.cs
  55. 39
      backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs
  56. 43
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs
  57. 34
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs
  58. 23
      backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs
  59. 50
      backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  60. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  61. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  62. 19
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
  63. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs
  64. 53
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  65. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs
  66. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  67. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs
  68. 61
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs
  69. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs
  70. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  71. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
  72. 2
      backend/src/Squidex.Domain.Apps.Entities/DomainEntityExtensions.cs
  73. 43
      backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs
  74. 82
      backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs
  75. 21
      backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs
  76. 16
      backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs
  77. 16
      backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs
  78. 17
      backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs
  79. 6
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
  80. 15
      backend/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs
  81. 34
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  82. 52
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs
  83. 42
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  84. 15
      backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs
  85. 1
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  86. 3
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs
  87. 9
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs
  88. 11
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs
  89. 12
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs
  90. 1
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs
  91. 17
      backend/src/Squidex.Infrastructure/CollectionExtensions.cs
  92. 118
      backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs
  93. 15
      backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
  94. 7
      backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
  95. 15
      backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs
  96. 3
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs
  97. 3
      backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs
  98. 18
      backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs
  99. 8
      backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs
  100. 9
      backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs

1
backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;

1
backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.Plugins; using Squidex.Infrastructure.Plugins;

10
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs

@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
Guard.NotNullOrEmpty(id); Guard.NotNullOrEmpty(id);
return new AppClients(Without(id)); return Without<AppClients>(id);
} }
[Pure] [Pure]
@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Core.Apps
throw new ArgumentException("Id already exists.", nameof(id)); throw new ArgumentException("Id already exists.", nameof(id));
} }
return new AppClients(With(id, client)); return With<AppClients>(id, client);
} }
[Pure] [Pure]
@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Core.Apps
throw new ArgumentException("Id already exists.", nameof(id)); throw new ArgumentException("Id already exists.", nameof(id));
} }
return new AppClients(With(id, new AppClient(id, secret, Role.Editor))); return With<AppClients>(id, new AppClient(id, secret, Role.Editor));
} }
[Pure] [Pure]
@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
return new AppClients(With(id, client.Rename(newName))); return With<AppClients>(id, client.Rename(newName), DeepComparer<AppClient>.Instance);
} }
[Pure] [Pure]
@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
return new AppClients(With(id, client.Update(role))); return With<AppClients>(id, client.Update(role), DeepComparer<AppClient>.Instance);
} }
} }
} }

4
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs

@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.Apps
Guard.NotNullOrEmpty(contributorId); Guard.NotNullOrEmpty(contributorId);
Guard.NotNullOrEmpty(role); Guard.NotNullOrEmpty(role);
return new AppContributors(With(contributorId, role)); return With<AppContributors>(contributorId, role, EqualityComparer<string>.Default);
} }
[Pure] [Pure]
@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
Guard.NotNullOrEmpty(contributorId); Guard.NotNullOrEmpty(contributorId);
return new AppContributors(Without(contributorId)); return Without<AppContributors>(contributorId);
} }
} }
} }

6
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs

@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Apps
[Pure] [Pure]
public AppPatterns Remove(Guid id) public AppPatterns Remove(Guid id)
{ {
return new AppPatterns(Without(id)); return Without<AppPatterns>(id);
} }
[Pure] [Pure]
@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.Apps
throw new ArgumentException("Id already exists.", nameof(id)); throw new ArgumentException("Id already exists.", nameof(id));
} }
return new AppPatterns(With(id, newPattern)); return With<AppPatterns>(id, newPattern);
} }
[Pure] [Pure]
@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
return new AppPatterns(With(id, appPattern.Update(name, pattern, message))); return With<AppPatterns>(id, appPattern.Update(name, pattern, message), DeepComparer<AppPattern>.Instance);
} }
} }
} }

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

@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
Guard.NotNull(language); Guard.NotNull(language);
return new LanguagesConfig(languages, languages[language]); return Create(languages, languages[language]);
} }
[Pure] [Pure]
@ -109,12 +109,11 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
Guard.NotNull(config); Guard.NotNull(config);
var newLanguages = var newLanguages = languages.With(config.Language, config);
new ArrayDictionary<Language, LanguageConfig>(languages.With(config.Language, config));
var newMaster = Master?.Language == config.Language ? config : Master; var newMaster = Master?.Language == config.Language ? config : Master;
return new LanguagesConfig(newLanguages, newMaster!); return Create(newLanguages, newMaster!);
} }
[Pure] [Pure]
@ -134,6 +133,16 @@ namespace Squidex.Domain.Apps.Core.Apps
newLanguages.Values.FirstOrDefault(x => x.Language == Master.Language) ?? newLanguages.Values.FirstOrDefault(x => x.Language == Master.Language) ??
newLanguages.Values.FirstOrDefault(); newLanguages.Values.FirstOrDefault();
return Create(newLanguages, newMaster);
}
private LanguagesConfig Create(ArrayDictionary<Language, LanguageConfig> newLanguages, LanguageConfig newMaster)
{
if (newLanguages.EqualsDictionary(languages, EqualityComparer<Language>.Default, DeepComparer<LanguageConfig>.Instance) && newMaster.Language.Equals(master.Language))
{
return this;
}
return new LanguagesConfig(newLanguages, newMaster); return new LanguagesConfig(newLanguages, newMaster);
} }

11
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs

@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.Apps
[Pure] [Pure]
public Roles Remove(string name) public Roles Remove(string name)
{ {
return new Roles(inner.Without(name)); return Create(inner.Without(name));
} }
[Pure] [Pure]
@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
return new Roles(inner.With(name, newRole)); return Create(inner.With(name, newRole));
} }
[Pure] [Pure]
@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
return new Roles(inner.With(name, role.Update(permissions))); return Create(inner.With(name, role.Update(permissions), DeepComparer<Role>.Instance));
} }
public static bool IsDefault(string role) public static bool IsDefault(string role)
@ -176,5 +176,10 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
return items.Where(x => !Defaults.ContainsKey(x.Key)).ToArray(); return items.Where(x => !Defaults.ContainsKey(x.Key)).ToArray();
} }
private Roles Create(ArrayDictionary<string, Role> newRoles)
{
return ReferenceEquals(inner, newRoles) ? this : new Roles(newRoles);
}
} }
} }

110
backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs

@ -0,0 +1,110 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Assets
{
public sealed class AssetMetadata : Dictionary<string, IJsonValue>
{
private static readonly char[] PathSeparators = { '.', '[', ']' };
public AssetMetadata SetPixelWidth(int value)
{
this["pixelWidth"] = JsonValue.Create(value);
return this;
}
public AssetMetadata SetPixelHeight(int value)
{
this["pixelHeight"] = JsonValue.Create(value);
return this;
}
public int? GetPixelWidth()
{
if (TryGetValue("pixelWidth", out var n) && n is JsonNumber number)
{
return (int)number.Value;
}
return null;
}
public int? GetPixelHeight()
{
if (TryGetValue("pixelHeight", out var n) && n is JsonNumber number)
{
return (int)number.Value;
}
return null;
}
public bool TryGetNumber(string name, out double result)
{
if (TryGetValue(name, out var v) && v is JsonNumber n)
{
result = n.Value;
return true;
}
result = 0;
return false;
}
public bool TryGetString(string name, [MaybeNullWhen(false)] out string result)
{
if (TryGetValue(name, out var v) && v is JsonString s)
{
result = s.Value;
return true;
}
result = null!;
return false;
}
public bool TryGetByPath(string? path, [MaybeNullWhen(false)] out object result)
{
return TryGetByPath(path?.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries), out result!);
}
public bool TryGetByPath(IEnumerable<string>? path, [MaybeNullWhen(false)] out object result)
{
result = this;
if (path == null || !path.Any())
{
return false;
}
result = null!;
if (!TryGetValue(path.First(), out var json))
{
return false;
}
json.TryGetByPath(path.Skip(1), out var temp);
result = temp!;
return true;
}
}
}

11
backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs → backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs

@ -1,14 +1,17 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Core.Assets
{ {
public interface IUpdateableEntityWithVersion public enum AssetType
{ {
long Version { get; set; } Unknown,
Image,
Audio,
Video
} }
} }

10
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.Contents
[Pure] [Pure]
public Workflows Remove(Guid id) public Workflows Remove(Guid id)
{ {
return new Workflows(Without(id)); return Without<Workflows>(id);
} }
[Pure] [Pure]
@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
Guard.NotNullOrEmpty(name); Guard.NotNullOrEmpty(name);
return new Workflows(With(workflowId, Workflow.CreateDefault(name))); return With<Workflows>(workflowId, Workflow.CreateDefault(name));
} }
[Pure] [Pure]
@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
Guard.NotNull(workflow); Guard.NotNull(workflow);
return new Workflows(With(Guid.Empty, workflow)); return With<Workflows>(Guid.Empty, workflow, DeepComparer<Workflow>.Instance);
} }
[Pure] [Pure]
@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
Guard.NotNull(workflow); Guard.NotNull(workflow);
return new Workflows(With(id, workflow)); return With<Workflows>(id, workflow, DeepComparer<Workflow>.Instance);
} }
[Pure] [Pure]
@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Core.Contents
return this; return this;
} }
return new Workflows(With(id, workflow)); return With<Workflows>(id, workflow, DeepComparer<Workflow>.Instance);
} }
public Workflow GetFirst() public Workflow GetFirst()

31
backend/src/Squidex.Domain.Apps.Core.Model/DeepComparer.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core
{
public sealed class DeepComparer<T> : IEqualityComparer<T>
{
public static readonly DeepComparer<T> Instance = new DeepComparer<T>();
private DeepComparer()
{
}
public bool Equals(T x, T y)
{
return x.IsDeepEqual(y);
}
public int GetHashCode(T obj)
{
return 0;
}
}
}

48
backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs

@ -43,16 +43,18 @@ namespace Squidex.Domain.Apps.Core.Rules
Guard.NotNull(trigger); Guard.NotNull(trigger);
Guard.NotNull(action); Guard.NotNull(action);
this.trigger = trigger; SetTrigger(trigger);
this.trigger.Freeze(); SetAction(action);
this.action = action;
this.action.Freeze();
} }
[Pure] [Pure]
public Rule Rename(string newName) public Rule Rename(string newName)
{ {
if (string.Equals(name, newName))
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.name = newName; clone.name = newName;
@ -62,6 +64,11 @@ namespace Squidex.Domain.Apps.Core.Rules
[Pure] [Pure]
public Rule Enable() public Rule Enable()
{ {
if (isEnabled)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isEnabled = true; clone.isEnabled = true;
@ -71,6 +78,11 @@ namespace Squidex.Domain.Apps.Core.Rules
[Pure] [Pure]
public Rule Disable() public Rule Disable()
{ {
if (!isEnabled)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isEnabled = false; clone.isEnabled = false;
@ -87,11 +99,14 @@ namespace Squidex.Domain.Apps.Core.Rules
throw new ArgumentException("New trigger has another type.", nameof(newTrigger)); throw new ArgumentException("New trigger has another type.", nameof(newTrigger));
} }
newTrigger.Freeze(); if (trigger.DeepEquals(newTrigger))
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.trigger = newTrigger; clone.SetTrigger(newTrigger);
}); });
} }
@ -105,12 +120,27 @@ namespace Squidex.Domain.Apps.Core.Rules
throw new ArgumentException("New action has another type.", nameof(newAction)); throw new ArgumentException("New action has another type.", nameof(newAction));
} }
newAction.Freeze(); if (action.DeepEquals(newAction))
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.action = newAction; clone.SetAction(newAction);
}); });
} }
private void SetAction(RuleAction newAction)
{
action = newAction;
action.Freeze();
}
private void SetTrigger(RuleTrigger newTrigger)
{
trigger = newTrigger;
trigger.Freeze();
}
} }
} }

6
backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs

@ -8,6 +8,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using DeepEqual.Syntax;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Core.Rules namespace Squidex.Domain.Apps.Core.Rules
@ -37,5 +38,10 @@ namespace Squidex.Domain.Apps.Core.Rules
{ {
yield break; yield break;
} }
public bool DeepEquals(RuleAction action)
{
return this.WithDeepEqual(action).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
} }
} }

7
backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs

@ -5,10 +5,17 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Rules namespace Squidex.Domain.Apps.Core.Rules
{ {
public abstract class RuleTrigger : Freezable public abstract class RuleTrigger : Freezable
{ {
public abstract T Accept<T>(IRuleTriggerVisitor<T> visitor); public abstract T Accept<T>(IRuleTriggerVisitor<T> visitor);
public bool DeepEquals(RuleTrigger action)
{
return this.WithDeepEqual(action).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
} }
} }

5
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs

@ -113,6 +113,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
throw new ArgumentException("Ids must cover all fields.", nameof(ids)); throw new ArgumentException("Ids must cover all fields.", nameof(ids));
} }
if (ids.SequenceEqual(fieldsOrdered.Select(x => x.Id)))
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToArray(); clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToArray();

6
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs

@ -8,6 +8,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
@ -44,5 +45,10 @@ namespace Squidex.Domain.Apps.Core.Schemas
return new FieldNames(list); return new FieldNames(list);
} }
public bool DeepEquals(FieldNames names)
{
return this.IsDeepEqual(names);
}
} }
} }

6
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
@ -26,5 +27,10 @@ namespace Squidex.Domain.Apps.Core.Schemas
public abstract RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null); public abstract RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null);
public abstract NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null); public abstract NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null);
public bool DeepEquals(FieldProperties properties)
{
return this.WithDeepEqual(properties).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
} }
} }

25
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs

@ -64,6 +64,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public NestedField Lock() public NestedField Lock()
{ {
if (isLocked)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isLocked = true; clone.isLocked = true;
@ -73,6 +78,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public NestedField Hide() public NestedField Hide()
{ {
if (isHidden)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isHidden = true; clone.isHidden = true;
@ -82,6 +92,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public NestedField Show() public NestedField Show()
{ {
if (!isHidden)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isHidden = false; clone.isHidden = false;
@ -91,6 +106,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public NestedField Disable() public NestedField Disable()
{ {
if (isDisabled)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isDisabled = true; clone.isDisabled = true;
@ -100,6 +120,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public NestedField Enable() public NestedField Enable()
{ {
if (!isDisabled)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isDisabled = false; clone.isDisabled = false;

7
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs

@ -36,6 +36,13 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
var typedProperties = ValidateProperties(newProperties); var typedProperties = ValidateProperties(newProperties);
typedProperties.Freeze();
if (properties.DeepEquals(typedProperties))
{
return this;
}
return Clone<NestedField<T>>(clone => return Clone<NestedField<T>>(clone =>
{ {
clone.SetProperties(typedProperties); clone.SetProperties(typedProperties);

25
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs

@ -73,6 +73,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public RootField Lock() public RootField Lock()
{ {
if (isLocked)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isLocked = true; clone.isLocked = true;
@ -82,6 +87,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public RootField Hide() public RootField Hide()
{ {
if (isHidden)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isHidden = true; clone.isHidden = true;
@ -91,6 +101,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public RootField Show() public RootField Show()
{ {
if (!isHidden)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isHidden = false; clone.isHidden = false;
@ -100,6 +115,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public RootField Disable() public RootField Disable()
{ {
if (isDisabled)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isDisabled = true; clone.isDisabled = true;
@ -109,6 +129,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public RootField Enable() public RootField Enable()
{ {
if (!isDisabled)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isDisabled = false; clone.isDisabled = false;

5
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs

@ -36,6 +36,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
var typedProperties = ValidateProperties(newProperties); var typedProperties = ValidateProperties(newProperties);
if (properties.DeepEquals(typedProperties))
{
return this;
}
return Clone<RootField<T>>(clone => return Clone<RootField<T>>(clone =>
{ {
clone.SetProperties(typedProperties); clone.SetProperties(typedProperties);

70
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs

@ -116,21 +116,33 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public Schema Update(SchemaProperties newProperties) public Schema Update(SchemaProperties newProperties)
{ {
Guard.NotNull(newProperties); newProperties ??= new SchemaProperties();
if (properties.DeepEquals(newProperties))
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.properties = newProperties; clone.properties = newProperties;
clone.properties.Freeze(); clone.Properties.Freeze();
}); });
} }
[Pure] [Pure]
public Schema ConfigureScripts(SchemaScripts newScripts) public Schema ConfigureScripts(SchemaScripts newScripts)
{ {
newScripts ??= new SchemaScripts();
if (scripts.DeepEquals(newScripts))
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.scripts = newScripts ?? new SchemaScripts(); clone.scripts = newScripts;
clone.scripts.Freeze(); clone.scripts.Freeze();
}); });
} }
@ -138,42 +150,55 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public Schema ConfigureFieldsInLists(FieldNames names) public Schema ConfigureFieldsInLists(FieldNames names)
{ {
names ??= FieldNames.Empty;
if (fieldsInLists.DeepEquals(names))
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.fieldsInLists = names ?? FieldNames.Empty; clone.fieldsInLists = names;
}); });
} }
[Pure] [Pure]
public Schema ConfigureFieldsInLists(params string[] names) public Schema ConfigureFieldsInLists(params string[] names)
{ {
return Clone(clone => return ConfigureFieldsInLists(new FieldNames(names));
{
clone.fieldsInLists = new FieldNames(names);
});
} }
[Pure] [Pure]
public Schema ConfigureFieldsInReferences(FieldNames names) public Schema ConfigureFieldsInReferences(FieldNames names)
{ {
names ??= FieldNames.Empty;
if (fieldsInReferences.DeepEquals(names))
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.fieldsInReferences = names ?? FieldNames.Empty; clone.fieldsInReferences = names;
}); });
} }
[Pure] [Pure]
public Schema ConfigureFieldsInReferences(params string[] names) public Schema ConfigureFieldsInReferences(params string[] names)
{ {
return Clone(clone => return ConfigureFieldsInReferences(new FieldNames(names));
{
clone.fieldsInReferences = new FieldNames(names);
});
} }
[Pure] [Pure]
public Schema Publish() public Schema Publish()
{ {
if (isPublished)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isPublished = true; clone.isPublished = true;
@ -183,6 +208,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public Schema Unpublish() public Schema Unpublish()
{ {
if (!isPublished)
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.isPublished = false; clone.isPublished = false;
@ -192,6 +222,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public Schema ChangeCategory(string newCategory) public Schema ChangeCategory(string newCategory)
{ {
if (string.Equals(category, newCategory))
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.category = newCategory; clone.category = newCategory;
@ -201,9 +236,16 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public Schema ConfigurePreviewUrls(IReadOnlyDictionary<string, string> newPreviewUrls) public Schema ConfigurePreviewUrls(IReadOnlyDictionary<string, string> newPreviewUrls)
{ {
previewUrls ??= EmptyPreviewUrls;
if (previewUrls.EqualsDictionary(newPreviewUrls))
{
return this;
}
return Clone(clone => return Clone(clone =>
{ {
clone.previewUrls = newPreviewUrls ?? EmptyPreviewUrls; clone.previewUrls = newPreviewUrls;
}); });
} }

6
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs

@ -6,11 +6,17 @@
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public sealed class SchemaProperties : NamedElementPropertiesBase public sealed class SchemaProperties : NamedElementPropertiesBase
{ {
public ReadOnlyCollection<string> Tags { get; set; } public ReadOnlyCollection<string> Tags { get; set; }
public bool DeepEquals(SchemaProperties properties)
{
return this.WithDeepEqual(properties).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
} }
} }

7
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public sealed class SchemaScripts : Freezable public sealed class SchemaScripts : Freezable
@ -25,5 +27,10 @@ namespace Squidex.Domain.Apps.Core.Schemas
public string Delete { get; set; } public string Delete { get; set; }
public string Query { get; set; } public string Query { get; set; }
public bool DeepEquals(SchemaScripts scripts)
{
return this.WithDeepEqual(scripts).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
} }
} }

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

@ -10,6 +10,7 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Fody" Version="4.2.1" PrivateAssets="all" /> <PackageReference Include="Fody" Version="4.2.1" PrivateAssets="all" />
<PackageReference Include="Freezable.Fody" Version="1.9.3" PrivateAssets="all" /> <PackageReference Include="Freezable.Fody" Version="1.9.3" PrivateAssets="all" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

15
backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs

@ -13,17 +13,15 @@ using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Schemas; using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.EventSynchronization namespace Squidex.Domain.Apps.Core.EventSynchronization
{ {
public static class SchemaSynchronizer public static class SchemaSynchronizer
{ {
public static IEnumerable<IEvent> Synchronize(this Schema source, Schema? target, IJsonSerializer serializer, Func<long> idGenerator, public static IEnumerable<IEvent> Synchronize(this Schema source, Schema? target, Func<long> idGenerator,
SchemaSynchronizationOptions? options = null) SchemaSynchronizationOptions? options = null)
{ {
Guard.NotNull(source); Guard.NotNull(source);
Guard.NotNull(serializer);
Guard.NotNull(idGenerator); Guard.NotNull(idGenerator);
if (target == null) if (target == null)
@ -39,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
return @event; return @event;
} }
if (!source.Properties.EqualsJson(target.Properties, serializer)) if (!source.Properties.DeepEquals(target.Properties))
{ {
yield return E(new SchemaUpdated { Properties = target.Properties }); yield return E(new SchemaUpdated { Properties = target.Properties });
} }
@ -49,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
yield return E(new SchemaCategoryChanged { Name = target.Category }); yield return E(new SchemaCategoryChanged { Name = target.Category });
} }
if (!source.Scripts.EqualsJson(target.Scripts, serializer)) if (!source.Scripts.DeepEquals(target.Scripts))
{ {
yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts }); yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts });
} }
@ -66,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
E(new SchemaUnpublished()); E(new SchemaUnpublished());
} }
var events = SyncFields(source.FieldCollection, target.FieldCollection, serializer, idGenerator, CanUpdateRoot, null, options); var events = SyncFields(source.FieldCollection, target.FieldCollection, idGenerator, CanUpdateRoot, null, options);
foreach (var @event in events) foreach (var @event in events)
{ {
@ -88,7 +86,6 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
private static IEnumerable<SchemaEvent> SyncFields<T>( private static IEnumerable<SchemaEvent> SyncFields<T>(
FieldCollection<T> source, FieldCollection<T> source,
FieldCollection<T> target, FieldCollection<T> target,
IJsonSerializer serializer,
Func<long> idGenerator, Func<long> idGenerator,
Func<T, T, bool> canUpdate, Func<T, T, bool> canUpdate,
NamedId<long>? parentId, SchemaSynchronizationOptions options) where T : class, IField NamedId<long>? parentId, SchemaSynchronizationOptions options) where T : class, IField
@ -131,7 +128,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
if (canUpdate(sourceField, targetField)) if (canUpdate(sourceField, targetField))
{ {
if (!sourceField.RawProperties.EqualsJson(targetField.RawProperties, serializer)) if (!sourceField.RawProperties.DeepEquals(targetField.RawProperties))
{ {
yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }); yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties });
} }
@ -194,7 +191,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
{ {
var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection<NestedField>.Empty; var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection<NestedField>.Empty;
var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, CanUpdate, id, options); var events = SyncFields(fields, targetArrayField.FieldCollection, idGenerator, CanUpdate, id, options);
foreach (var @event in events) foreach (var @event in events)
{ {

9
backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.EventSynchronization namespace Squidex.Domain.Apps.Core.EventSynchronization
{ {
@ -26,13 +25,5 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
{ {
return lhs.GetType() == rhs.GetType(); return lhs.GetType() == rhs.GetType();
} }
public static bool EqualsJson<T>(this T lhs, T rhs, IJsonSerializer serializer)
{
var lhsJson = serializer.Serialize(lhs);
var rhsJson = serializer.Serialize(rhs);
return string.Equals(lhsJson, rhsJson);
}
} }
} }

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

@ -13,6 +13,7 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
{ {
public sealed class EdmTypeVisitor : IFieldVisitor<IEdmTypeReference?> public sealed class EdmTypeVisitor : IFieldVisitor<IEdmTypeReference?>
{ {
private static readonly EdmComplexType JsonType = new EdmComplexType("Squidex", "Json", null, false, true);
private readonly EdmTypeFactory typeFactory; private readonly EdmTypeFactory typeFactory;
internal EdmTypeVisitor(EdmTypeFactory typeFactory) internal EdmTypeVisitor(EdmTypeFactory typeFactory)
@ -67,7 +68,7 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
public IEdmTypeReference? Visit(IField<JsonFieldProperties> field) public IEdmTypeReference? Visit(IField<JsonFieldProperties> field)
{ {
return null; return new EdmComplexTypeReference(JsonType, !field.RawProperties.IsRequired);
} }
public IEdmTypeReference? Visit(IField<NumberFieldProperties> field) public IEdmTypeReference? Visit(IField<NumberFieldProperties> field)

31
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs

@ -26,39 +26,52 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
return new JsonSchema { Type = JsonObjectType.String }; return new JsonSchema { Type = JsonObjectType.String };
} }
public static JsonSchemaProperty ArrayProperty(JsonSchema item) public static JsonSchemaProperty ArrayProperty(JsonSchema item, string? description = null, bool isRequired = false)
{ {
return new JsonSchemaProperty { Type = JsonObjectType.Array, Item = item }; return Enrich(new JsonSchemaProperty { Type = JsonObjectType.Array, Item = item }, description, isRequired);
} }
public static JsonSchemaProperty BooleanProperty() public static JsonSchemaProperty BooleanProperty(string? description = null, bool isRequired = false)
{ {
return new JsonSchemaProperty { Type = JsonObjectType.Boolean }; return Enrich(new JsonSchemaProperty { Type = JsonObjectType.Boolean }, description, isRequired);
} }
public static JsonSchemaProperty DateTimeProperty(string? description = null, bool isRequired = false) public static JsonSchemaProperty DateTimeProperty(string? description = null, bool isRequired = false)
{ {
return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime, Description = description, IsRequired = isRequired }; return Enrich(new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime }, description, isRequired);
} }
public static JsonSchemaProperty GuidProperty(string? description = null, bool isRequired = false) public static JsonSchemaProperty GuidProperty(string? description = null, bool isRequired = false)
{ {
return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid, Description = description, IsRequired = isRequired }; return Enrich(new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid }, description, isRequired);
} }
public static JsonSchemaProperty NumberProperty(string? description = null, bool isRequired = false) public static JsonSchemaProperty NumberProperty(string? description = null, bool isRequired = false)
{ {
return new JsonSchemaProperty { Type = JsonObjectType.Number, Description = description, IsRequired = isRequired }; return Enrich(new JsonSchemaProperty { Type = JsonObjectType.Number }, description, isRequired);
} }
public static JsonSchemaProperty ObjectProperty(JsonSchema item, string? description = null, bool isRequired = false) public static JsonSchemaProperty ObjectProperty(JsonSchema item, string? description = null, bool isRequired = false)
{ {
return new JsonSchemaProperty { Type = JsonObjectType.Object, Reference = item, Description = description, IsRequired = isRequired }; return Enrich(new JsonSchemaProperty { Type = JsonObjectType.Object, Reference = item }, description, isRequired);
} }
public static JsonSchemaProperty StringProperty(string? description = null, bool isRequired = false) public static JsonSchemaProperty StringProperty(string? description = null, bool isRequired = false)
{ {
return new JsonSchemaProperty { Type = JsonObjectType.String, Description = description, IsRequired = isRequired }; return Enrich(new JsonSchemaProperty { Type = JsonObjectType.String }, description, isRequired);
}
public static JsonSchemaProperty JsonProperty(string? description = null, bool isRequired = false)
{
return Enrich(new JsonSchemaProperty(), description, isRequired);
}
private static JsonSchemaProperty Enrich(JsonSchemaProperty property, string? description = null, bool isRequired = false)
{
property.Description = description;
property.IsRequired = isRequired;
return property;
} }
} }
} }

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

@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public JsonSchemaProperty? Visit(IField<JsonFieldProperties> field) public JsonSchemaProperty? Visit(IField<JsonFieldProperties> field)
{ {
return Builder.StringProperty(); return Builder.JsonProperty();
} }
public JsonSchemaProperty? Visit(IField<NumberFieldProperties> field) public JsonSchemaProperty? Visit(IField<NumberFieldProperties> field)

19
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
@ -286,28 +287,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return Fallback; return Fallback;
} }
for (var j = 2; j < path.Length; j++) if (path.Skip(2).Any())
{ {
if (value is JsonObject obj && obj.TryGetValue(path[j], out value)) if (!value.TryGetByPath(path.Skip(2), out value) || value == null || value.Type == JsonValueType.Null)
{
continue;
}
if (value is JsonArray array && int.TryParse(path[j], out var idx) && idx >= 0 && idx < array.Count)
{
value = array[idx];
}
else
{ {
return Fallback; return Fallback;
} }
} }
if (value == null || value.Type == JsonValueType.Null)
{
return Fallback;
}
return value.ToString() ?? Fallback; return value.ToString() ?? Fallback;
} }
} }

11
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Squidex.Domain.Apps.Core.Assets;
namespace Squidex.Domain.Apps.Core.ValidateContent namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
@ -15,16 +16,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
long FileSize { get; } long FileSize { get; }
bool IsImage { get; }
int? PixelWidth { get; }
int? PixelHeight { get; }
string FileName { get; } string FileName { get; }
string FileHash { get; } string FileHash { get; }
string Slug { get; } string Slug { get; }
AssetMetadata Metadata { get; }
AssetType Type { get; }
} }
} }

13
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -61,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
addError(path, "Invalid file extension."); addError(path, "Invalid file extension.");
} }
if (!asset.IsImage) if (asset.Type != AssetType.Image)
{ {
if (properties.MustBeImage) if (properties.MustBeImage)
{ {
@ -71,11 +72,13 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
continue; continue;
} }
if (asset.PixelWidth.HasValue && var pixelWidth = asset.Metadata.GetPixelWidth();
asset.PixelHeight.HasValue) var pixelHeight = asset.Metadata.GetPixelHeight();
if (pixelWidth.HasValue && pixelHeight.HasValue)
{ {
var w = asset.PixelWidth.Value; var w = pixelWidth.Value;
var h = asset.PixelHeight.Value; var h = pixelHeight.Value;
var actualRatio = (double)w / h; var actualRatio = (double)w / h;

19
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs

@ -10,8 +10,10 @@ using System.Collections.Generic;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{ {
@ -67,21 +69,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonElement("fv")] [BsonElement("fv")]
public long FileVersion { get; set; } public long FileVersion { get; set; }
[BsonRequired]
[BsonElement("im")]
public bool IsImage { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement("vs")] [BsonElement("vs")]
public long Version { get; set; } public long Version { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement("pw")] [BsonElement("at")]
public int? PixelWidth { get; set; } public AssetType Type { get; set; }
[BsonRequired]
[BsonElement("ph")]
public int? PixelHeight { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement("cb")] [BsonElement("cb")]
@ -99,6 +93,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonElement("dl")] [BsonElement("dl")]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
[BsonJson]
[BsonRequired]
[BsonElement("md")]
public AssetMetadata Metadata { get; set; }
public Guid AssetId public Guid AssetId
{ {
get { return Id; } get { return Id; }

6
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
@ -19,19 +18,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
public static class FindExtensions public static class FindExtensions
{ {
private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter; private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter;
private static readonly SortDefinitionBuilder<MongoAssetEntity> Sorting = Builders<MongoAssetEntity>.Sort;
public static ClrQuery AdjustToModel(this ClrQuery query) public static ClrQuery AdjustToModel(this ClrQuery query)
{ {
if (query.Filter != null) if (query.Filter != null)
{ {
query.Filter = PascalCasePathConverter<ClrValue>.Transform(query.Filter); query.Filter = FirstPascalPathConverter<ClrValue>.Transform(query.Filter);
} }
query.Sort = query.Sort query.Sort = query.Sort
.Select(x => .Select(x =>
new SortNode( new SortNode(
x.Path.Select(p => p.ToPascalCase()).ToList(), x.Path.ToFirstPascalCase(),
x.Order)) x.Order))
.ToList(); .ToList();

30
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathConverter.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public sealed class FirstPascalPathConverter<TValue> : TransformVisitor<TValue>
{
private static readonly FirstPascalPathConverter<TValue> Instance = new FirstPascalPathConverter<TValue>();
private FirstPascalPathConverter()
{
}
public static FilterNode<TValue>? Transform(FilterNode<TValue> node)
{
return node.Accept(Instance);
}
public override FilterNode<TValue>? Visit(CompareFilter<TValue> nodeIn)
{
return new CompareFilter<TValue>(nodeIn.Path.ToFirstPascalCase(), nodeIn.Operator, nodeIn.Value);
}
}
}

25
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathExtension.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public static class FirstPascalPathExtension
{
public static PropertyPath ToFirstPascalCase(this PropertyPath path)
{
var result = path.ToList();
result[0] = result[0].ToPascalCase();
return new PropertyPath(result);
}
}
}

6
backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
@ -73,11 +72,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
e("Plan can only changed from the user who configured the plan initially."); e("Plan can only changed from the user who configured the plan initially.");
} }
if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase))
{
e("App has already this plan.");
}
}); });
} }
} }

9
backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -52,14 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
throw new DomainForbiddenException("You cannot change your own role."); throw new DomainForbiddenException("You cannot change your own role.");
} }
if (contributors.TryGetValue(command.ContributorId, out var role)) if (!contributors.TryGetValue(command.ContributorId, out var role))
{
if (role == command.Role)
{
e(Not.New("Contributor", "role"), nameof(command.Role));
}
}
else
{ {
if (plan != null && plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors) if (plan != null && plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors)
{ {

209
backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
@ -56,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
[DataMember] [DataMember]
public bool IsArchived { get; set; } public bool IsArchived { get; set; }
public void ApplyEvent(IEvent @event) public override bool ApplyEvent(IEvent @event)
{ {
switch (@event) switch (@event)
{ {
@ -64,189 +65,171 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
{ {
SimpleMapper.Map(e, this); SimpleMapper.Map(e, this);
break; return true;
} }
case AppUpdated e: case AppUpdated e when !string.Equals(e.Label, Label) || !string.Equals(e.Description, Description):
{ {
SimpleMapper.Map(e, this); SimpleMapper.Map(e, this);
break; return true;
} }
case AppImageUploaded e: case AppImageUploaded e:
{ return UpdateImage(e, ev => ev.Image);
Image = e.Image;
break;
}
case AppImageRemoved _: case AppImageRemoved e when Image != null:
{ return UpdateImage(e, ev => null);
Image = null;
break;
}
case AppPlanChanged e: case AppPlanChanged e when !string.Equals(Plan?.PlanId, e.PlanId):
{ return UpdatePlan(e, ev => AppPlan.Build(ev.Actor, ev.PlanId));
Plan = AppPlan.Build(e.Actor, e.PlanId);
break;
}
case AppPlanReset _:
{
Plan = null;
break; case AppPlanReset e when Plan != null:
} return UpdatePlan(e, ev => null);
case AppContributorAssigned e: case AppContributorAssigned e:
{ return UpdateContributors(e, (ev, c) => c.Assign(ev.ContributorId, ev.Role));
Contributors = Contributors.Assign(e.ContributorId, e.Role);
break;
}
case AppContributorRemoved e: case AppContributorRemoved e:
{ return UpdateContributors(e, (ev, c) => c.Remove(ev.ContributorId));
Contributors = Contributors.Remove(e.ContributorId);
break;
}
case AppClientAttached e: case AppClientAttached e:
{ return UpdateClients(e, (ev, c) => c.Add(ev.Id, ev.Secret));
Clients = Clients.Add(e.Id, e.Secret);
break;
}
case AppClientUpdated e: case AppClientUpdated e:
{ return UpdateClients(e, (ev, c) => c.Update(ev.Id, ev.Role));
Clients = Clients.Update(e.Id, e.Role);
break;
}
case AppClientRenamed e: case AppClientRenamed e:
{ return UpdateClients(e, (ev, c) => c.Rename(ev.Id, ev.Name));
Clients = Clients.Rename(e.Id, e.Name);
break;
}
case AppClientRevoked e: case AppClientRevoked e:
{ return UpdateClients(e, (ev, c) => c.Revoke(ev.Id));
Clients = Clients.Revoke(e.Id);
break;
}
case AppWorkflowAdded e: case AppWorkflowAdded e:
{ return UpdateWorkflows(e, (ev, w) => w.Add(ev.WorkflowId, ev.Name));
Workflows = Workflows.Add(e.WorkflowId, e.Name);
break;
}
case AppWorkflowUpdated e: case AppWorkflowUpdated e:
{ return UpdateWorkflows(e, (ev, w) => w.Update(ev.WorkflowId, ev.Workflow));
Workflows = Workflows.Update(e.WorkflowId, e.Workflow);
break;
}
case AppWorkflowDeleted e: case AppWorkflowDeleted e:
{ return UpdateWorkflows(e, (ev, w) => w.Remove(ev.WorkflowId));
Workflows = Workflows.Remove(e.WorkflowId);
break;
}
case AppPatternAdded e: case AppPatternAdded e:
{ return UpdatePatterns(e, (ev, p) => p.Add(ev.PatternId, ev.Name, ev.Pattern, ev.Message));
Patterns = Patterns.Add(e.PatternId, e.Name, e.Pattern, e.Message);
break;
}
case AppPatternDeleted e: case AppPatternDeleted e:
{ return UpdatePatterns(e, (ev, p) => p.Remove(ev.PatternId));
Patterns = Patterns.Remove(e.PatternId);
break;
}
case AppPatternUpdated e: case AppPatternUpdated e:
return UpdatePatterns(e, (ev, p) => p.Update(ev.PatternId, ev.Name, ev.Pattern, ev.Message));
case AppRoleAdded e:
return UpdateRoles(e, (ev, r) => r.Add(ev.Name));
case AppRoleUpdated e:
return UpdateRoles(e, (ev, r) => r.Update(ev.Name, ev.Permissions));
case AppRoleDeleted e:
return UpdateRoles(e, (ev, r) => r.Remove(ev.Name));
case AppLanguageAdded e:
return UpdateLanguages(e, (ev, l) => l.Set(ev.Language));
case AppLanguageRemoved e:
return UpdateLanguages(e, (ev, l) => l.Remove(ev.Language));
case AppLanguageUpdated e:
return UpdateLanguages(e, (ev, l) =>
{ {
Patterns = Patterns.Update(e.PatternId, e.Name, e.Pattern, e.Message); l = l.Set(ev.Language, ev.IsOptional, ev.Fallback);
break; if (ev.IsMaster)
{
LanguagesConfig = LanguagesConfig.MakeMaster(ev.Language);
} }
case AppRoleAdded e: return l;
});
case AppArchived _:
{ {
Roles = Roles.Add(e.Name); Plan = null;
break; IsArchived = true;
return true;
}
} }
case AppRoleDeleted e: return false;
}
private bool UpdateContributors<T>(T @event, Func<T, AppContributors, AppContributors> update)
{ {
Roles = Roles.Remove(e.Name); var previous = Contributors;
break; Contributors = update(@event, previous);
return !ReferenceEquals(previous, Contributors);
} }
case AppRoleUpdated e: private bool UpdateClients<T>(T @event, Func<T, AppClients, AppClients> update)
{ {
Roles = Roles.Update(e.Name, e.Permissions); var previous = Clients;
Clients = update(@event, previous);
break; return !ReferenceEquals(previous, Clients);
} }
case AppLanguageAdded e: private bool UpdateLanguages<T>(T @event, Func<T, LanguagesConfig, LanguagesConfig> update)
{ {
LanguagesConfig = LanguagesConfig.Set(e.Language); var previous = LanguagesConfig;
LanguagesConfig = update(@event, previous);
break; return !ReferenceEquals(previous, LanguagesConfig);
} }
case AppLanguageRemoved e: private bool UpdatePatterns<T>(T @event, Func<T, AppPatterns, AppPatterns> update)
{ {
LanguagesConfig = LanguagesConfig.Remove(e.Language); var previous = Patterns;
break; Patterns = update(@event, previous);
return !ReferenceEquals(previous, Patterns);
} }
case AppLanguageUpdated e: private bool UpdateRoles<T>(T @event, Func<T, Roles, Roles> update)
{ {
LanguagesConfig = LanguagesConfig.Set(e.Language, e.IsOptional, e.Fallback); var previous = Roles;
if (e.IsMaster) Roles = update(@event, previous);
{
LanguagesConfig = LanguagesConfig.MakeMaster(e.Language);
}
break; return !ReferenceEquals(previous, Roles);
} }
case AppArchived _: private bool UpdateWorkflows<T>(T @event, Func<T, Workflows, Workflows> update)
{ {
Plan = null; var previous = Workflows;
IsArchived = true; Workflows = update(@event, previous);
break; return !ReferenceEquals(previous, Workflows);
}
} }
private bool UpdateImage<T>(T @event, Func<T, AppImage?> update)
{
Image = update(@event);
return true;
} }
public override AppState Apply(Envelope<IEvent> @event) private bool UpdatePlan<T>(T @event, Func<T, AppPlan?> update)
{ {
return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); Plan = update(@event);
return true;
} }
} }
} }

36
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -11,7 +11,6 @@ using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -23,33 +22,29 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly IAssetFileStore assetFileStore; private readonly IAssetFileStore assetFileStore;
private readonly IAssetEnricher assetEnricher; private readonly IAssetEnricher assetEnricher;
private readonly IAssetQueryService assetQuery; private readonly IAssetQueryService assetQuery;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IContextProvider contextProvider; private readonly IContextProvider contextProvider;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators; private readonly IEnumerable<IAssetMetadataSource> assetMetadataSources;
public AssetCommandMiddleware( public AssetCommandMiddleware(
IGrainFactory grainFactory, IGrainFactory grainFactory,
IAssetEnricher assetEnricher, IAssetEnricher assetEnricher,
IAssetQueryService assetQuery,
IAssetFileStore assetFileStore, IAssetFileStore assetFileStore,
IAssetThumbnailGenerator assetThumbnailGenerator, IAssetQueryService assetQuery,
IContextProvider contextProvider, IContextProvider contextProvider,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators) IEnumerable<IAssetMetadataSource> assetMetadataSources)
: base(grainFactory) : base(grainFactory)
{ {
Guard.NotNull(assetEnricher); Guard.NotNull(assetEnricher);
Guard.NotNull(assetFileStore); Guard.NotNull(assetFileStore);
Guard.NotNull(assetQuery); Guard.NotNull(assetQuery);
Guard.NotNull(assetThumbnailGenerator); Guard.NotNull(assetMetadataSources);
Guard.NotNull(contextProvider); Guard.NotNull(contextProvider);
Guard.NotNull(tagGenerators);
this.assetFileStore = assetFileStore; this.assetFileStore = assetFileStore;
this.assetEnricher = assetEnricher; this.assetEnricher = assetEnricher;
this.assetQuery = assetQuery; this.assetQuery = assetQuery;
this.assetThumbnailGenerator = assetThumbnailGenerator;
this.contextProvider = contextProvider; this.contextProvider = contextProvider;
this.tagGenerators = tagGenerators; this.assetMetadataSources = assetMetadataSources;
} }
public override async Task HandleAsync(CommandContext context, Func<Task> next) public override async Task HandleAsync(CommandContext context, Func<Task> next)
@ -60,7 +55,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
case CreateAsset createAsset: case CreateAsset createAsset:
{ {
await EnrichWithImageInfosAsync(createAsset);
await EnrichWithHashAndUploadAsync(createAsset, tempFile); await EnrichWithHashAndUploadAsync(createAsset, tempFile);
try try
@ -82,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
} }
} }
GenerateTags(createAsset); await EnrichWithMetadataAsync(createAsset, createAsset.Tags);
await HandleCoreAsync(context, next); await HandleCoreAsync(context, next);
@ -102,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
case UpdateAsset updateAsset: case UpdateAsset updateAsset:
{ {
await EnrichWithImageInfosAsync(updateAsset); await EnrichWithMetadataAsync(updateAsset);
await EnrichWithHashAndUploadAsync(updateAsset, tempFile); await EnrichWithHashAndUploadAsync(updateAsset, tempFile);
try try
@ -144,11 +138,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
return asset?.FileName == file.FileName && asset.FileSize == file.FileSize; return asset?.FileName == file.FileName && asset.FileSize == file.FileSize;
} }
private async Task EnrichWithImageInfosAsync(UploadAssetCommand command)
{
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
}
private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile) private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile)
{ {
using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256)) using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256))
@ -159,16 +148,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
} }
} }
private void GenerateTags(CreateAsset createAsset) private async Task EnrichWithMetadataAsync(UploadAssetCommand command, HashSet<string>? tags = null)
{
if (createAsset.Tags == null)
{ {
createAsset.Tags = new HashSet<string>(); foreach (var metadataSource in assetMetadataSources)
}
foreach (var tagGenerator in tagGenerators)
{ {
tagGenerator.GenerateTags(createAsset, createAsset.Tags); await metadataSource.EnhanceAsync(command, tags);
} }
} }
} }

9
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
@ -42,17 +43,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
public string Slug { get; set; } public string Slug { get; set; }
public string MetadataText { get; set; }
public long FileSize { get; set; } public long FileSize { get; set; }
public long FileVersion { get; set; } public long FileVersion { get; set; }
public bool IsImage { get; set; }
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
public int? PixelWidth { get; set; } public AssetMetadata Metadata { get; set; }
public int? PixelHeight { get; set; } public AssetType Type { get; set; }
public Guid AssetId public Guid AssetId
{ {

2
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs

@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
case RenameAssetFolder renameAssetFolder: case RenameAssetFolder renameAssetFolder:
return UpdateReturn(renameAssetFolder, c => return UpdateReturn(renameAssetFolder, c =>
{ {
GuardAssetFolder.CanRename(c, Snapshot.FolderName); GuardAssetFolder.CanRename(c);
Rename(c); Rename(c);

10
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
case AnnotateAsset annotateAsset: case AnnotateAsset annotateAsset:
return UpdateReturnAsync(annotateAsset, async c => return UpdateReturnAsync(annotateAsset, async c =>
{ {
GuardAsset.CanAnnotate(c, Snapshot.FileName!, Snapshot.Slug); GuardAsset.CanAnnotate(c);
var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags); var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
@ -126,13 +126,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
var @event = SimpleMapper.Map(command, new AssetCreated var @event = SimpleMapper.Map(command, new AssetCreated
{ {
IsImage = command.ImageInfo != null,
FileName = command.File.FileName, FileName = command.File.FileName,
FileSize = command.File.FileSize, FileSize = command.File.FileSize,
FileVersion = 0, FileVersion = 0,
MimeType = command.File.MimeType, MimeType = command.File.MimeType,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
Slug = command.File.FileName.ToAssetSlug() Slug = command.File.FileName.ToAssetSlug()
}); });
@ -147,10 +144,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
FileVersion = Snapshot.FileVersion + 1, FileVersion = Snapshot.FileVersion + 1,
FileSize = command.File.FileSize, FileSize = command.File.FileSize,
MimeType = command.File.MimeType, MimeType = command.File.MimeType
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
IsImage = command.ImageInfo != null
}); });
RaiseEvent(@event); RaiseEvent(@event);

3
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
@ -16,5 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public string? Slug { get; set; } public string? Slug { get; set; }
public HashSet<string> Tags { get; set; } public HashSet<string> Tags { get; set; }
public AssetMetadata Metadata { get; set; }
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public Guid ParentId { get; set; } public Guid ParentId { get; set; }
public HashSet<string> Tags { get; set; } public HashSet<string> Tags { get; } = new HashSet<string>();
public CreateAsset() public CreateAsset()
{ {

5
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
@ -13,7 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public AssetFile File { get; set; } public AssetFile File { get; set; }
public ImageInfo? ImageInfo { get; set; } public AssetMetadata Metadata { get; } = new AssetMetadata();
public AssetType Type { get; set; }
public string FileHash { get; set; } public string FileHash { get; set; }
} }

222
backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs

@ -0,0 +1,222 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Tasks;
using TagLib;
using TagLib.Image;
using static TagLib.File;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class FileTagAssetMetadataSource : IAssetMetadataSource
{
private sealed class FileAbstraction : IFileAbstraction
{
private readonly AssetFile file;
public string Name
{
get { return file.FileName; }
}
public Stream ReadStream
{
get { return file.OpenRead(); }
}
public Stream WriteStream
{
get { throw new NotSupportedException(); }
}
public FileAbstraction(AssetFile file)
{
this.file = file;
}
public void CloseStream(Stream stream)
{
stream.Close();
}
}
public Task EnhanceAsync(UploadAssetCommand command, HashSet<string>? tags)
{
Enhance(command, tags);
return TaskHelper.Done;
}
private void Enhance(UploadAssetCommand command, HashSet<string>? tags)
{
try
{
using (var file = Create(new FileAbstraction(command.File), ReadStyle.Average))
{
if (file.Properties == null)
{
return;
}
var type = file.Properties.MediaTypes;
if (type == MediaTypes.Audio)
{
command.Type = AssetType.Audio;
}
else if (type == MediaTypes.Photo)
{
command.Type = AssetType.Image;
}
else if (type.HasFlag(MediaTypes.Video))
{
command.Type = AssetType.Video;
}
var pw = file.Properties.PhotoWidth;
var ph = file.Properties.PhotoHeight;
if (pw > 0 && pw > 0)
{
command.Metadata.SetPixelWidth(pw);
command.Metadata.SetPixelHeight(ph);
if (tags != null)
{
tags.Add("image");
var wh = pw + ph;
if (wh > 2000)
{
tags.Add("image/large");
}
else if (wh > 1000)
{
tags.Add("image/medium");
}
else
{
tags.Add("image/small");
}
}
}
void TryAddString(string name, string? value)
{
if (!string.IsNullOrWhiteSpace(value))
{
command.Metadata.Add(name, JsonValue.Create(value));
}
}
void TryAddInt(string name, int? value)
{
if (value > 0)
{
command.Metadata.Add(name, JsonValue.Create(value));
}
}
void TryAddDouble(string name, double? value)
{
if (value > 0)
{
command.Metadata.Add(name, JsonValue.Create(value));
}
}
void TryAddTimeSpan(string name, TimeSpan value)
{
if (value != TimeSpan.Zero)
{
command.Metadata.Add(name, JsonValue.Create(value.ToString()));
}
}
if (file.Tag is ImageTag imageTag)
{
TryAddDouble("locationLatitude", imageTag.Latitude);
TryAddDouble("locationLongitude", imageTag.Longitude);
TryAddString("created", imageTag.DateTime?.ToIso8601());
}
TryAddTimeSpan("duration", file.Properties.Duration);
TryAddInt("audioBitrate", file.Properties.AudioBitrate);
TryAddInt("audioChannels", file.Properties.AudioChannels);
TryAddInt("audioSampleRate", file.Properties.AudioSampleRate);
TryAddInt("bitsPerSample", file.Properties.BitsPerSample);
TryAddInt("imageQuality", file.Properties.PhotoQuality);
TryAddInt("videoWidth", file.Properties.VideoWidth);
TryAddInt("videoHeight", file.Properties.VideoHeight);
TryAddString("description", file.Properties.Description);
}
}
catch
{
return;
}
}
public IEnumerable<string> Format(IAssetEntity asset)
{
var metadata = asset.Metadata;
switch (asset.Type)
{
case AssetType.Image:
{
if (metadata.TryGetNumber("pixelWidth", out var w) &&
metadata.TryGetNumber("pixelHeight", out var h))
{
yield return $"{w}x{h}px";
}
break;
}
case AssetType.Video:
{
if (metadata.TryGetNumber("videoWidth", out var w) &&
metadata.TryGetNumber("videoHeight", out var h))
{
yield return $"{w}x{h}pt";
}
if (metadata.TryGetString("duration", out var duration))
{
yield return duration;
}
break;
}
case AssetType.Audio:
{
if (metadata.TryGetString("duration", out var duration))
{
yield return duration;
}
break;
}
}
}
}
}

19
backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs

@ -6,22 +6,33 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class FileTypeTagGenerator : ITagGenerator<CreateAsset> public sealed class FileTypeTagGenerator : IAssetMetadataSource
{ {
public void GenerateTags(CreateAsset source, HashSet<string> tags) public Task EnhanceAsync(UploadAssetCommand command, HashSet<string>? tags)
{ {
var extension = source.File?.FileName?.FileType(); if (tags != null)
{
var extension = command.File?.FileName?.FileType();
if (!string.IsNullOrWhiteSpace(extension)) if (!string.IsNullOrWhiteSpace(extension))
{ {
tags.Add($"type/{extension.ToLowerInvariant()}"); tags.Add($"type/{extension.ToLowerInvariant()}");
} }
} }
return TaskHelper.Done;
}
public IEnumerable<string> Format(IAssetEntity asset)
{
yield break;
}
} }
} }

19
backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
{ {
public static class GuardAsset public static class GuardAsset
{ {
public static void CanAnnotate(AnnotateAsset command, string oldFileName, string oldSlug) public static void CanAnnotate(AnnotateAsset command)
{ {
Guard.NotNull(command); Guard.NotNull(command);
@ -23,9 +23,14 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
{ {
if (string.IsNullOrWhiteSpace(command.FileName) && if (string.IsNullOrWhiteSpace(command.FileName) &&
string.IsNullOrWhiteSpace(command.Slug) && string.IsNullOrWhiteSpace(command.Slug) &&
command.Tags == null) command.Tags == null &&
{ command.Metadata == null)
e("Either file name, slug or tags must be defined.", nameof(command.FileName), nameof(command.Slug), nameof(command.Tags)); {
e("Either file name, slug, tags or metadata must be defined.",
nameof(command.FileName),
nameof(command.Slug),
nameof(command.Tags),
nameof(command.Metadata));
} }
}); });
} }
@ -46,11 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
return Validate.It(() => "Cannot move asset.", async e => return Validate.It(() => "Cannot move asset.", async e =>
{ {
if (command.ParentId == oldParentId) if (command.ParentId != oldParentId)
{
e("Asset is already part of this folder.", nameof(command.ParentId));
}
else
{ {
await CheckPathAsync(command.ParentId, assetQuery, e); await CheckPathAsync(command.ParentId, assetQuery, e);
} }

12
backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
}); });
} }
public static void CanRename(RenameAssetFolder command, string olderFolderName) public static void CanRename(RenameAssetFolder command)
{ {
Guard.NotNull(command); Guard.NotNull(command);
@ -40,10 +40,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
{ {
e(Not.Defined("Folder name"), nameof(command.FolderName)); e(Not.Defined("Folder name"), nameof(command.FolderName));
} }
else if (string.Equals(command.FolderName, olderFolderName))
{
e(Not.New("Asset folder", "name"), nameof(command.FolderName));
}
}); });
} }
@ -53,11 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
return Validate.It(() => "Cannot move asset.", async e => return Validate.It(() => "Cannot move asset.", async e =>
{ {
if (command.ParentId == oldParentId) if (command.ParentId != oldParentId)
{
e("Asset folder is already part of this folder.", nameof(command.ParentId));
}
else
{ {
await CheckPathAsync(command.ParentId, assetQuery, id, e); await CheckPathAsync(command.ParentId, assetQuery, id, e);
} }

20
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IAssetMetadataSource
{
Task EnhanceAsync(UploadAssetCommand command, HashSet<string>? tags);
IEnumerable<string> Format(IAssetEntity asset);
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs

@ -12,5 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
public interface IEnrichedAssetEntity : IAssetEntity public interface IEnrichedAssetEntity : IAssetEntity
{ {
HashSet<string> TagNames { get; } HashSet<string> TagNames { get; }
string MetadataText { get; }
} }
} }

69
backend/src/Squidex.Domain.Apps.Entities/Assets/ImageMetadataSource.cs

@ -0,0 +1,69 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class ImageMetadataSource : IAssetMetadataSource
{
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
public ImageMetadataSource(IAssetThumbnailGenerator assetThumbnailGenerator)
{
Guard.NotNull(assetThumbnailGenerator);
this.assetThumbnailGenerator = assetThumbnailGenerator;
}
public async Task EnhanceAsync(UploadAssetCommand command, HashSet<string>? tags)
{
var imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
if (imageInfo != null)
{
command.Type = AssetType.Image;
command.Metadata.SetPixelWidth(imageInfo.PixelWidth);
command.Metadata.SetPixelHeight(imageInfo.PixelHeight);
if (tags != null)
{
tags.Add("image");
var wh = imageInfo.PixelWidth + imageInfo.PixelHeight;
if (wh > 2000)
{
tags.Add("image/large");
}
else if (wh > 1000)
{
tags.Add("image/medium");
}
else
{
tags.Add("image/small");
}
}
}
}
public IEnumerable<string> Format(IAssetEntity asset)
{
if (asset.Type == AssetType.Image)
{
yield return $"{asset.Metadata.GetPixelWidth()}x{asset.Metadata.GetPixelHeight()}px";
}
}
}
}

39
backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs

@ -1,39 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class ImageTagGenerator : ITagGenerator<CreateAsset>
{
public void GenerateTags(CreateAsset source, HashSet<string> tags)
{
if (source.ImageInfo != null)
{
tags.Add("image");
var wh = source.ImageInfo.PixelWidth + source.ImageInfo.PixelHeight;
if (wh > 2000)
{
tags.Add("image/large");
}
else if (wh > 1000)
{
tags.Add("image/medium");
}
else
{
tags.Add("image/small");
}
}
}
}
}

43
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs

@ -7,6 +7,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -18,12 +19,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
public sealed class AssetEnricher : IAssetEnricher public sealed class AssetEnricher : IAssetEnricher
{ {
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly IEnumerable<IAssetMetadataSource> assetMetadataSources;
public AssetEnricher(ITagService tagService) public AssetEnricher(ITagService tagService, IEnumerable<IAssetMetadataSource> assetMetadataSources)
{ {
Guard.NotNull(tagService); Guard.NotNull(tagService);
Guard.NotNull(assetMetadataSources);
this.tagService = tagService; this.tagService = tagService;
this.assetMetadataSources = assetMetadataSources;
} }
public async Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset, Context context) public async Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset, Context context)
@ -48,12 +52,49 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
if (ShouldEnrich(context)) if (ShouldEnrich(context))
{ {
await EnrichTagsAsync(results); await EnrichTagsAsync(results);
EnrichWithMetadataText(results);
} }
return results; return results;
} }
} }
private void EnrichWithMetadataText(List<AssetEntity> results)
{
var sb = new StringBuilder();
void Append(string? text)
{
if (!string.IsNullOrWhiteSpace(text))
{
if (sb.Length > 0)
{
sb.Append(", ");
}
sb.Append(text);
}
}
foreach (var asset in results)
{
sb.Clear();
foreach (var source in assetMetadataSources)
{
foreach (var metadata in source.Format(asset))
{
Append(metadata);
}
}
Append(asset.FileSize.ToReadableSize());
asset.MetadataText = sb.ToString();
}
}
private async Task EnrichTagsAsync(List<AssetEntity> assets) private async Task EnrichTagsAsync(List<AssetEntity> assets)
{ {
foreach (var group in assets.GroupBy(x => x.AppId.Id)) foreach (var group in assets.GroupBy(x => x.AppId.Id))

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

@ -116,19 +116,18 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
AddProperty(nameof(IAssetEntity.Id), JsonObjectType.String, JsonFormatStrings.Guid); AddProperty(nameof(IAssetEntity.Id), JsonObjectType.String, JsonFormatStrings.Guid);
AddProperty(nameof(IAssetEntity.Created), JsonObjectType.String, JsonFormatStrings.DateTime); AddProperty(nameof(IAssetEntity.Created), JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty(nameof(IAssetEntity.CreatedBy), JsonObjectType.String); AddProperty(nameof(IAssetEntity.CreatedBy), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.LastModified), JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty(nameof(IAssetEntity.LastModifiedBy), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Version), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.FileName), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.FileHash), JsonObjectType.String); AddProperty(nameof(IAssetEntity.FileHash), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.FileName), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.FileSize), JsonObjectType.Integer); AddProperty(nameof(IAssetEntity.FileSize), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.FileVersion), JsonObjectType.Integer); AddProperty(nameof(IAssetEntity.FileVersion), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.IsImage), JsonObjectType.Boolean); AddProperty(nameof(IAssetEntity.LastModified), JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty(nameof(IAssetEntity.LastModifiedBy), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Metadata), JsonObjectType.None);
AddProperty(nameof(IAssetEntity.MimeType), JsonObjectType.String); AddProperty(nameof(IAssetEntity.MimeType), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.PixelHeight), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.PixelWidth), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.Slug), JsonObjectType.String); AddProperty(nameof(IAssetEntity.Slug), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Tags), JsonObjectType.String); AddProperty(nameof(IAssetEntity.Tags), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Type), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Version), JsonObjectType.Integer);
return schema; return schema;
} }
@ -142,22 +141,29 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
entityType.AddStructuralProperty(name.ToCamelCase(), 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(nameof(IAssetEntity.Metadata), new EdmComplexTypeReference(jsonType, false));
AddProperty(nameof(IAssetEntity.Id), EdmPrimitiveTypeKind.String); AddProperty(nameof(IAssetEntity.Id), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Created), EdmPrimitiveTypeKind.DateTimeOffset); AddProperty(nameof(IAssetEntity.Created), EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty(nameof(IAssetEntity.CreatedBy), EdmPrimitiveTypeKind.String); AddProperty(nameof(IAssetEntity.CreatedBy), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty(nameof(IAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Version), EdmPrimitiveTypeKind.Int64);
AddProperty(nameof(IAssetEntity.FileName), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.FileHash), EdmPrimitiveTypeKind.String); AddProperty(nameof(IAssetEntity.FileHash), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.FileName), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.FileSize), EdmPrimitiveTypeKind.Int64); AddProperty(nameof(IAssetEntity.FileSize), EdmPrimitiveTypeKind.Int64);
AddProperty(nameof(IAssetEntity.FileVersion), EdmPrimitiveTypeKind.Int64); AddProperty(nameof(IAssetEntity.FileVersion), EdmPrimitiveTypeKind.Int64);
AddProperty(nameof(IAssetEntity.IsImage), EdmPrimitiveTypeKind.Boolean); AddProperty(nameof(IAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty(nameof(IAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.MimeType), EdmPrimitiveTypeKind.String); AddProperty(nameof(IAssetEntity.MimeType), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.PixelHeight), EdmPrimitiveTypeKind.Int32);
AddProperty(nameof(IAssetEntity.PixelWidth), EdmPrimitiveTypeKind.Int32);
AddProperty(nameof(IAssetEntity.Slug), EdmPrimitiveTypeKind.String); AddProperty(nameof(IAssetEntity.Slug), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Tags), EdmPrimitiveTypeKind.String); AddProperty(nameof(IAssetEntity.Tags), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Type), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Version), EdmPrimitiveTypeKind.Int64);
var container = new EdmEntityContainer("Squidex", "Container"); var container = new EdmEntityContainer("Squidex", "Container");

23
backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs

@ -16,7 +16,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Assets.State namespace Squidex.Domain.Apps.Entities.Assets.State
{ {
public class AssetFolderState : DomainObjectState<AssetFolderState>, IAssetFolderEntity public sealed class AssetFolderState : DomainObjectState<AssetFolderState>, IAssetFolderEntity
{ {
[DataMember] [DataMember]
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }
@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
[DataMember] [DataMember]
public Guid ParentId { get; set; } public Guid ParentId { get; set; }
public void ApplyEvent(IEvent @event) public override bool ApplyEvent(IEvent @event)
{ {
switch (@event) switch (@event)
{ {
@ -38,35 +38,32 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
{ {
SimpleMapper.Map(e, this); SimpleMapper.Map(e, this);
break; return true;
} }
case AssetFolderRenamed e: case AssetFolderRenamed e when e.FolderName != FolderName:
{ {
SimpleMapper.Map(e, this); FolderName = e.FolderName;
break; return true;
} }
case AssetFolderMoved e: case AssetFolderMoved e when e.ParentId != ParentId:
{ {
ParentId = e.ParentId; ParentId = e.ParentId;
break; return true;
} }
case AssetFolderDeleted _: case AssetFolderDeleted _:
{ {
IsDeleted = true; IsDeleted = true;
break; return true;
}
} }
} }
public override AssetFolderState Apply(Envelope<IEvent> @event) return false;
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
} }
} }
} }

50
backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
@ -47,26 +48,23 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
public long TotalSize { get; set; } public long TotalSize { get; set; }
[DataMember] [DataMember]
public bool IsImage { get; set; } public HashSet<string> Tags { get; set; }
[DataMember] [DataMember]
public int? PixelWidth { get; set; } public AssetMetadata Metadata { get; set; }
[DataMember] [DataMember]
public int? PixelHeight { get; set; } public AssetType Type { get; set; }
[DataMember] [DataMember]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
[DataMember]
public HashSet<string> Tags { get; set; }
public Guid AssetId public Guid AssetId
{ {
get { return Id; } get { return Id; }
} }
public void ApplyEvent(IEvent @event) public override bool ApplyEvent(IEvent @event)
{ {
switch (@event) switch (@event)
{ {
@ -87,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
TotalSize += e.FileSize; TotalSize += e.FileSize;
break; return true;
} }
case AssetUpdated e: case AssetUpdated e:
@ -96,48 +94,60 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
TotalSize += e.FileSize; TotalSize += e.FileSize;
break; return true;
} }
case AssetAnnotated e: case AssetAnnotated e:
{ {
if (!string.IsNullOrWhiteSpace(e.FileName)) var hasChanged = false;
if (!string.IsNullOrWhiteSpace(e.FileName) && !string.Equals(e.FileName, FileName))
{ {
FileName = e.FileName; FileName = e.FileName;
hasChanged = true;
} }
if (!string.IsNullOrWhiteSpace(e.Slug)) if (!string.IsNullOrWhiteSpace(e.Slug) && !string.Equals(e.Slug, Slug))
{ {
Slug = e.Slug; Slug = e.Slug;
hasChanged = true;
} }
if (e.Tags != null) if (e.Tags != null && !e.Tags.SetEquals(Tags))
{ {
Tags = e.Tags; Tags = e.Tags;
hasChanged = true;
} }
break; if (e.Metadata != null && !e.Metadata.EqualsDictionary(Metadata))
{
Metadata = e.Metadata;
hasChanged = true;
} }
case AssetMoved e: return hasChanged;
}
case AssetMoved e when e.ParentId != ParentId:
{ {
ParentId = e.ParentId; ParentId = e.ParentId;
break; return true;
} }
case AssetDeleted _: case AssetDeleted _:
{ {
IsDeleted = true; IsDeleted = true;
break; return true;
}
} }
} }
public override AssetState Apply(Envelope<IEvent> @event) return false;
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
} }
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs

@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return partitionResolver(key); return partitionResolver(key);
} }
public (IGraphType? ResolveType, ValueResolver? Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName) public (IGraphType?, ValueResolver?, QueryArguments?) GetGraphType(ISchemaEntity schema, IField field, string fieldName)
{ {
return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, this, assetListType, fieldName)); return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, this, assetListType, fieldName));
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs

@ -33,6 +33,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IObjectGraphType GetContentType(Guid schemaId); IObjectGraphType GetContentType(Guid schemaId);
(IGraphType? ResolveType, ValueResolver? Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName); (IGraphType?, ValueResolver?, QueryArguments?) GetGraphType(ISchemaEntity schema, IField field, string fieldName);
} }
} }

19
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs

@ -7,12 +7,15 @@
using System; using System;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public static class AllTypes public static class AllTypes
{ {
public const string PathName = "path";
public static readonly Type None = typeof(NoopGraphType); public static readonly Type None = typeof(NoopGraphType);
public static readonly Type NonNullTagsType = typeof(NonNullGraphType<ListGraphType<NonNullGraphType<StringGraphType>>>); public static readonly Type NonNullTagsType = typeof(NonNullGraphType<ListGraphType<NonNullGraphType<StringGraphType>>>);
@ -31,6 +34,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType Boolean = new BooleanGraphType(); public static readonly IGraphType Boolean = new BooleanGraphType();
public static readonly IGraphType AssetType = new EnumerationGraphType<AssetType>();
public static readonly IGraphType NonNullInt = new NonNullGraphType(Int); public static readonly IGraphType NonNullInt = new NonNullGraphType(Int);
public static readonly IGraphType NonNullGuid = new NonNullGraphType(Guid); public static readonly IGraphType NonNullGuid = new NonNullGraphType(Guid);
@ -43,6 +48,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType NonNullBoolean = new NonNullGraphType(Boolean); public static readonly IGraphType NonNullBoolean = new NonNullGraphType(Boolean);
public static readonly IGraphType NonNullAssetType = new NonNullGraphType(AssetType);
public static readonly IGraphType NoopDate = new NoopGraphType(Date); public static readonly IGraphType NoopDate = new NoopGraphType(Date);
public static readonly IGraphType NoopJson = new NoopGraphType(Json); public static readonly IGraphType NoopJson = new NoopGraphType(Json);
@ -53,8 +60,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType NoopBoolean = new NoopGraphType(Boolean); public static readonly IGraphType NoopBoolean = new NoopGraphType(Boolean);
public static readonly IGraphType NoopTags = new NoopGraphType("Tags"); public static readonly IGraphType NoopTags = new NoopGraphType("TagsScalar");
public static readonly IGraphType NoopGeolocation = new NoopGraphType("Geolocation"); public static readonly IGraphType NoopGeolocation = new NoopGraphType("GeolocationScalar");
public static readonly QueryArguments PathArguments = new QueryArguments(new QueryArgument(None)
{
Name = PathName,
Description = $"The path to the json value",
DefaultValue = null,
ResolvedType = String
});
} }
} }

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType AddField(new FieldType
{ {
Name = $"find{schemaType}Content", Name = $"find{schemaType}Content",
Arguments = CreateContentFindTypes(schemaName), Arguments = CreateContentFindArguments(schemaName),
ResolvedType = contentType, ResolvedType = contentType,
Resolver = ResolveAsync((c, e) => Resolver = ResolveAsync((c, e) =>
{ {
@ -149,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}; };
} }
private static QueryArguments CreateContentFindTypes(string schemaName) private static QueryArguments CreateContentFindArguments(string schemaName)
{ {
return new QueryArguments return new QueryArguments
{ {

53
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs

@ -8,6 +8,7 @@
using System; using System;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -143,24 +144,43 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "isImage", Name = "isImage",
ResolvedType = AllTypes.NonNullBoolean, ResolvedType = AllTypes.NonNullBoolean,
Resolver = Resolve(x => x.IsImage), Resolver = Resolve(x => x.Type == AssetType.Image),
Description = "Determines of the created file is an image." Description = "Determines if the uploaded file is an image.",
DeprecationReason = "Use 'type' field instead."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "pixelWidth", Name = "pixelWidth",
ResolvedType = AllTypes.Int, ResolvedType = AllTypes.Int,
Resolver = Resolve(x => x.PixelWidth), Resolver = Resolve(x => x.Metadata.GetPixelWidth()),
Description = "The width of the image in pixels if the asset is an image." Description = "The width of the image in pixels if the asset is an image.",
DeprecationReason = "Use 'metadata' field instead."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "pixelHeight", Name = "pixelHeight",
ResolvedType = AllTypes.Int, ResolvedType = AllTypes.Int,
Resolver = Resolve(x => x.PixelHeight), Resolver = Resolve(x => x.Metadata.GetPixelHeight()),
Description = "The height of the image in pixels if the asset is an image." Description = "The height of the image in pixels if the asset is an image.",
DeprecationReason = "Use 'metadata' field instead."
});
AddField(new FieldType
{
Name = "type",
ResolvedType = AllTypes.NonNullAssetType,
Resolver = Resolve(x => x.Type),
Description = "The type of the image."
});
AddField(new FieldType
{
Name = "metadataText",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.MetadataText),
Description = "The text representation of the metadata."
}); });
AddField(new FieldType AddField(new FieldType
@ -172,6 +192,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Type = AllTypes.NonNullTagsType Type = AllTypes.NonNullTagsType
}); });
AddField(new FieldType
{
Name = "metadata",
Arguments = AllTypes.PathArguments,
ResolvedType = AllTypes.NoopJson,
Resolver = ResolveMetadata(),
Description = "The asset metadata.",
});
if (model.CanGenerateAssetSourceUrl) if (model.CanGenerateAssetSourceUrl)
{ {
AddField(new FieldType AddField(new FieldType
@ -186,6 +215,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "An asset"; Description = "An asset";
} }
private static IFieldResolver ResolveMetadata()
{
return new FuncFieldResolver<IEnrichedAssetEntity, object?>(c =>
{
var path = c.Arguments.GetOrDefault(AllTypes.PathName);
c.Source.Metadata.TryGetByPath(path as string, out var result);
return result;
});
}
private static IFieldResolver Resolve(Func<IEnrichedAssetEntity, object?> action) private static IFieldResolver Resolve(Func<IEnrichedAssetEntity, object?> action)
{ {
return new FuncFieldResolver<IEnrichedAssetEntity, object?>(c => action(c.Source)); return new FuncFieldResolver<IEnrichedAssetEntity, object?>(c => action(c.Source));

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs

@ -20,13 +20,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
foreach (var (field, fieldName, _) in schema.SchemaDef.Fields.SafeFields()) foreach (var (field, fieldName, _) in schema.SchemaDef.Fields.SafeFields())
{ {
var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); var (resolvedType, valueResolver, args) = model.GetGraphType(schema, field, fieldName);
if (valueResolver != null) if (valueResolver != null)
{ {
AddField(new FieldType AddField(new FieldType
{ {
Name = fieldName, Name = fieldName,
Arguments = args,
Resolver = PartitionResolver(valueResolver, field.Name), Resolver = PartitionResolver(valueResolver, field.Name),
ResolvedType = resolvedType, ResolvedType = resolvedType,
Description = field.RawProperties.Hints Description = field.RawProperties.Hints

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields()) foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields())
{ {
var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); var (resolvedType, valueResolver, args) = model.GetGraphType(schema, field, fieldName);
if (valueResolver != null) if (valueResolver != null)
{ {
@ -44,6 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
fieldGraphType.AddField(new FieldType fieldGraphType.AddField(new FieldType
{ {
Name = key.EscapePartition(), Name = key.EscapePartition(),
Arguments = args,
Resolver = PartitionResolver(valueResolver, key), Resolver = PartitionResolver(valueResolver, key),
ResolvedType = resolvedType, ResolvedType = resolvedType,
Description = field.RawProperties.Hints Description = field.RawProperties.Hints

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs

@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields()) foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields())
{ {
var (resolveType, valueResolver) = model.GetGraphType(schema, nestedField, nestedName); var (resolveType, valueResolver, args) = model.GetGraphType(schema, nestedField, nestedName);
if (resolveType != null && valueResolver != null) if (resolveType != null && valueResolver != null)
{ {
@ -35,6 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType AddField(new FieldType
{ {
Name = nestedName, Name = nestedName,
Arguments = args,
Resolver = resolver, Resolver = resolver,
ResolvedType = resolveType, ResolvedType = resolveType,
Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field."

61
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public delegate object ValueResolver(IJsonValue value, ResolveFieldContext context); public delegate object ValueResolver(IJsonValue value, ResolveFieldContext context);
public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType? ResolveType, ValueResolver? Resolver)> public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType?, ValueResolver?, QueryArguments?)>
{ {
private static readonly ValueResolver NoopResolver = (value, c) => value; private static readonly ValueResolver NoopResolver = (value, c) => value;
private readonly Dictionary<Guid, ContentGraphType> schemaTypes; private readonly Dictionary<Guid, ContentGraphType> schemaTypes;
@ -40,74 +40,83 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
this.fieldName = fieldName; this.fieldName = fieldName;
} }
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IArrayField field) public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IArrayField field)
{ {
return ResolveNested(field); return ResolveNested(field);
} }
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<AssetsFieldProperties> field) public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<AssetsFieldProperties> field)
{ {
return ResolveAssets(); return ResolveAssets();
} }
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<BooleanFieldProperties> field) public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<BooleanFieldProperties> field)
{ {
return ResolveDefault(AllTypes.NoopBoolean); return ResolveDefault(AllTypes.NoopBoolean);
} }
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<DateTimeFieldProperties> field) public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<DateTimeFieldProperties> field)
{ {
return ResolveDefault(AllTypes.NoopDate); return ResolveDefault(AllTypes.NoopDate);
} }
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<GeolocationFieldProperties> field) public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<GeolocationFieldProperties> field)
{ {
return ResolveDefault(AllTypes.NoopGeolocation); return ResolveDefault(AllTypes.NoopGeolocation);
} }
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<JsonFieldProperties> field) public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<NumberFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopJson);
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<NumberFieldProperties> field)
{ {
return ResolveDefault(AllTypes.NoopFloat); return ResolveDefault(AllTypes.NoopFloat);
} }
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<ReferencesFieldProperties> field) public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<ReferencesFieldProperties> field)
{ {
return ResolveReferences(field); return ResolveReferences(field);
} }
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<StringFieldProperties> field) public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<StringFieldProperties> field)
{ {
return ResolveDefault(AllTypes.NoopString); return ResolveDefault(AllTypes.NoopString);
} }
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<TagsFieldProperties> field) public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<TagsFieldProperties> field)
{ {
return ResolveDefault(AllTypes.NoopTags); return ResolveDefault(AllTypes.NoopTags);
} }
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<UIFieldProperties> field) public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<UIFieldProperties> field)
{ {
return (null, null); return (null, null, null);
} }
private static (IGraphType? ResolveType, ValueResolver? Resolver) ResolveDefault(IGraphType type) private static (IGraphType?, ValueResolver?, QueryArguments?) ResolveDefault(IGraphType type)
{ {
return (type, NoopResolver); return (type, NoopResolver, null);
} }
private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveNested(IArrayField field) private (IGraphType?, ValueResolver?, QueryArguments?) ResolveNested(IArrayField field)
{ {
var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field, fieldName))); var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field, fieldName)));
return (schemaFieldType, NoopResolver); return (schemaFieldType, NoopResolver, null);
}
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<JsonFieldProperties> field)
{
var resolver = new ValueResolver((value, c) =>
{
var path = c.Arguments.GetOrDefault(AllTypes.PathName);
value.TryGetByPath(path as string, out var result);
return result!;
});
return (AllTypes.NoopJson, resolver, AllTypes.PathArguments);
} }
private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveAssets() private (IGraphType?, ValueResolver?, QueryArguments?) ResolveAssets()
{ {
var resolver = new ValueResolver((value, c) => var resolver = new ValueResolver((value, c) =>
{ {
@ -116,10 +125,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return context.GetReferencedAssetsAsync(value); return context.GetReferencedAssetsAsync(value);
}); });
return (assetListType, resolver); return (assetListType, resolver, null);
} }
private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveReferences(IField<ReferencesFieldProperties> field) private (IGraphType?, ValueResolver?, QueryArguments?) ResolveReferences(IField<ReferencesFieldProperties> field)
{ {
IGraphType contentType = schemaTypes.GetOrDefault(field.Properties.SingleId()); IGraphType contentType = schemaTypes.GetOrDefault(field.Properties.SingleId());
@ -129,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
if (!union.PossibleTypes.Any()) if (!union.PossibleTypes.Any())
{ {
return (null, null); return (null, null, null);
} }
contentType = union; contentType = union;
@ -144,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType)); var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType));
return (schemaFieldType, resolver); return (schemaFieldType, resolver, null);
} }
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs

@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
{ {
public JsonGraphType() public JsonGraphType()
{ {
Name = "Json"; Name = "JsonScalar";
Description = "Unstructured Json object"; Description = "Unstructured Json object";
} }

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

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.ExtractReferenceIds;
@ -249,7 +250,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
field.GetReferencedIds(partitionValue, Ids.ContentOnly) field.GetReferencedIds(partitionValue, Ids.ContentOnly)
.Select(x => assets[x]) .Select(x => assets[x])
.SelectMany(x => x) .SelectMany(x => x)
.FirstOrDefault(x => x.IsImage); .FirstOrDefault(x => x.Type == AssetType.Image);
if (referencedImage != null) if (referencedImage != null)
{ {

9
backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs

@ -17,7 +17,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Contents.State namespace Squidex.Domain.Apps.Entities.Contents.State
{ {
public class ContentState : DomainObjectState<ContentState>, IContentEntity public sealed class ContentState : DomainObjectState<ContentState>, IContentEntity
{ {
[DataMember] [DataMember]
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }
@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.State
[DataMember] [DataMember]
public Status Status { get; set; } public Status Status { get; set; }
public void ApplyEvent(IEvent @event) public override bool ApplyEvent(IEvent @event)
{ {
switch (@event) switch (@event)
{ {
@ -121,11 +121,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.State
break; break;
} }
} }
}
public override ContentState Apply(Envelope<IEvent> @event) return true;
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
} }
private void UpdateData(NamedContentData? data, NamedContentData? dataDraft, bool isPending) private void UpdateData(NamedContentData? data, NamedContentData? dataDraft, bool isPending)

2
backend/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs → backend/src/Squidex.Domain.Apps.Entities/DomainEntityExtensions.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
{ {
public static class EntityExtensions public static class DomainEntityExtensions
{ {
public static NamedId<Guid> NamedId(this IAppEntity entity) public static NamedId<Guid> NamedId(this IAppEntity entity)
{ {

43
backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs

@ -8,22 +8,20 @@
using System; using System;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
{ {
public abstract class DomainObjectState<T> : Cloneable<T>, public abstract class DomainObjectState<T> :
IDomainState<T>, IDomainState<T>,
IEntity, IEntity,
IEntityWithCreatedBy, IEntityWithCreatedBy,
IEntityWithLastModifiedBy, IEntityWithLastModifiedBy,
IEntityWithVersion, IEntityWithVersion
IUpdateableEntity, where T : class
IUpdateableEntityWithCreatedBy,
IUpdateableEntityWithLastModifiedBy
where T : Cloneable
{ {
[DataMember] [DataMember]
public Guid Id { get; set; } public Guid Id { get; set; }
@ -43,11 +41,38 @@ namespace Squidex.Domain.Apps.Entities
[DataMember] [DataMember]
public long Version { get; set; } = EtagVersion.Empty; public long Version { get; set; } = EtagVersion.Empty;
public abstract T Apply(Envelope<IEvent> @event); public abstract bool ApplyEvent(IEvent @event);
public T Clone() public T Apply(Envelope<IEvent> @event)
{ {
return Clone(x => { }); var payload = (SquidexEvent)@event.Payload;
var clone = (DomainObjectState<T>)MemberwiseClone();
if (!clone.ApplyEvent(@event.Payload))
{
return (this as T)!;
}
var headers = @event.Headers;
if (clone.Id == default)
{
clone.Id = headers.AggregateId();
}
if (clone.CreatedBy == null)
{
clone.Created = headers.Timestamp();
clone.CreatedBy = payload.Actor;
}
clone.LastModified = headers.Timestamp();
clone.LastModifiedBy = payload.Actor;
clone.Version = headers.EventStreamNumber();
return (clone as T)!;
} }
} }
} }

82
backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs

@ -1,82 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities
{
public static class EntityMapper
{
public static T Update<T>(this T entity, Envelope<IEvent> envelope, Action<SquidexEvent, T>? updater = null) where T : IEntity
{
var @event = (SquidexEvent)envelope.Payload;
var headers = envelope.Headers;
SetId(entity, headers);
SetCreated(entity, headers);
SetCreatedBy(entity, @event);
SetLastModified(entity, headers);
SetLastModifiedBy(entity, @event);
SetVersion(entity, headers);
updater?.Invoke(@event, entity);
return entity;
}
private static void SetId(IEntity entity, EnvelopeHeaders headers)
{
if (entity is IUpdateableEntity updateable && updateable.Id == Guid.Empty)
{
updateable.Id = headers.AggregateId();
}
}
private static void SetVersion(IEntity entity, EnvelopeHeaders headers)
{
if (entity is IUpdateableEntityWithVersion updateable)
{
updateable.Version = headers.EventStreamNumber();
}
}
private static void SetCreated(IEntity entity, EnvelopeHeaders headers)
{
if (entity is IUpdateableEntity updateable && updateable.Created == default)
{
updateable.Created = headers.Timestamp();
}
}
private static void SetCreatedBy(IEntity entity, SquidexEvent @event)
{
if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy.CreatedBy == null)
{
withCreatedBy.CreatedBy = @event.Actor;
}
}
private static void SetLastModified(IEntity entity, EnvelopeHeaders headers)
{
if (entity is IUpdateableEntity updateable)
{
updateable.LastModified = headers.Timestamp();
}
}
private static void SetLastModifiedBy(IEntity entity, SquidexEvent @event)
{
if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy)
{
withModifiedBy.LastModifiedBy = @event.Actor;
}
}
}
}

21
backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs

@ -1,21 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Domain.Apps.Entities
{
public interface IUpdateableEntity
{
Guid Id { get; set; }
Instant Created { get; set; }
Instant LastModified { get; set; }
}
}

16
backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs

@ -1,16 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities
{
public interface IUpdateableEntityWithCreatedBy
{
RefToken CreatedBy { get; set; }
}
}

16
backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs

@ -1,16 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities
{
public interface IUpdateableEntityWithLastModifiedBy
{
RefToken LastModifiedBy { get; set; }
}
}

17
backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs

@ -7,7 +7,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
@ -46,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
}); });
} }
public static Task CanUpdate(UpdateRule command, Guid appId, IAppProvider appProvider, Rule rule) public static Task CanUpdate(UpdateRule command, Guid appId, IAppProvider appProvider)
{ {
Guard.NotNull(command); Guard.NotNull(command);
@ -73,24 +72,14 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
}); });
} }
public static void CanEnable(EnableRule command, Rule rule) public static void CanEnable(EnableRule command)
{ {
Guard.NotNull(command); Guard.NotNull(command);
if (rule.IsEnabled)
{
throw new DomainException("Rule is already enabled.");
}
} }
public static void CanDisable(DisableRule command, Rule rule) public static void CanDisable(DisableRule command)
{ {
Guard.NotNull(command); Guard.NotNull(command);
if (!rule.IsEnabled)
{
throw new DomainException("Rule is already disabled.");
}
} }
public static void CanDelete(DeleteRule command) public static void CanDelete(DeleteRule command)

6
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs

@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
case UpdateRule updateRule: case UpdateRule updateRule:
return UpdateReturnAsync(updateRule, async c => return UpdateReturnAsync(updateRule, async c =>
{ {
await GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider, Snapshot.RuleDef); await GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider);
Update(c); Update(c);
@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
case EnableRule enableRule: case EnableRule enableRule:
return UpdateReturn(enableRule, c => return UpdateReturn(enableRule, c =>
{ {
GuardRule.CanEnable(c, Snapshot.RuleDef); GuardRule.CanEnable(c);
Enable(c); Enable(c);
@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
case DisableRule disableRule: case DisableRule disableRule:
return UpdateReturn(disableRule, c => return UpdateReturn(disableRule, c =>
{ {
GuardRule.CanDisable(c, Snapshot.RuleDef); GuardRule.CanDisable(c);
Disable(c); Disable(c);

15
backend/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs

@ -18,7 +18,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Rules.State namespace Squidex.Domain.Apps.Entities.Rules.State
{ {
[CollectionName("Rules")] [CollectionName("Rules")]
public class RuleState : DomainObjectState<RuleState>, IRuleEntity public sealed class RuleState : DomainObjectState<RuleState>, IRuleEntity
{ {
[DataMember] [DataMember]
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }
@ -29,8 +29,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.State
[DataMember] [DataMember]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
public void ApplyEvent(IEvent @event) public override bool ApplyEvent(IEvent @event)
{ {
var previousRule = RuleDef;
switch (@event) switch (@event)
{ {
case RuleCreated e: case RuleCreated e:
@ -40,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.State
AppId = e.AppId; AppId = e.AppId;
break; return true;
} }
case RuleUpdated e: case RuleUpdated e:
@ -81,14 +83,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.State
{ {
IsDeleted = true; IsDeleted = true;
break; return true;
}
} }
} }
public override RuleState Apply(Envelope<IEvent> @event) return !ReferenceEquals(previousRule, RuleDef);
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
} }
} }
} }

34
backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs

@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
}); });
} }
public static void CanReorder(Schema schema, ReorderFields command) public static void CanReorder(ReorderFields command, Schema schema)
{ {
Guard.NotNull(command); Guard.NotNull(command);
@ -88,53 +88,43 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
}); });
} }
public static void CanPublish(Schema schema, PublishSchema command) public static void CanConfigureUIFields(ConfigureUIFields command, Schema schema)
{ {
Guard.NotNull(command); Guard.NotNull(command);
if (schema.IsPublished) Validate.It(() => "Cannot configure UI fields.", e =>
{ {
throw new DomainException("Schema is already published."); ValidateFieldNames(schema, command.FieldsInLists, nameof(command.FieldsInLists), e, IsMetaField);
} ValidateFieldNames(schema, command.FieldsInReferences, nameof(command.FieldsInReferences), e, IsNotAllowed);
});
} }
public static void CanUnpublish(Schema schema, UnpublishSchema command) public static void CanPublish(PublishSchema command)
{ {
Guard.NotNull(command); Guard.NotNull(command);
if (!schema.IsPublished)
{
throw new DomainException("Schema is not published.");
}
} }
public static void CanConfigureUIFields(Schema schema, ConfigureUIFields command) public static void CanUnpublish(UnpublishSchema command)
{ {
Guard.NotNull(command); Guard.NotNull(command);
Validate.It(() => "Cannot configure UI fields.", e =>
{
ValidateFieldNames(schema, command.FieldsInLists, nameof(command.FieldsInLists), e, IsMetaField);
ValidateFieldNames(schema, command.FieldsInReferences, nameof(command.FieldsInReferences), e, IsNotAllowed);
});
} }
public static void CanUpdate(Schema schema, UpdateSchema command) public static void CanUpdate(UpdateSchema command)
{ {
Guard.NotNull(command); Guard.NotNull(command);
} }
public static void CanConfigureScripts(Schema schema, ConfigureScripts command) public static void CanConfigureScripts(ConfigureScripts command)
{ {
Guard.NotNull(command); Guard.NotNull(command);
} }
public static void CanChangeCategory(Schema schema, ChangeCategory command) public static void CanChangeCategory(ChangeCategory command)
{ {
Guard.NotNull(command); Guard.NotNull(command);
} }
public static void CanDelete(Schema schema, DeleteSchema command) public static void CanDelete(DeleteSchema command)
{ {
Guard.NotNull(command); Guard.NotNull(command);
} }

52
backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{ {
public static class GuardSchemaField public static class GuardSchemaField
{ {
public static void CanAdd(Schema schema, AddField command) public static void CanAdd(AddField command, Schema schema)
{ {
Guard.NotNull(command); Guard.NotNull(command);
@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
}); });
} }
public static void CanUpdate(Schema schema, UpdateField command) public static void CanUpdate(UpdateField command, Schema schema)
{ {
Guard.NotNull(command); Guard.NotNull(command);
@ -82,86 +82,66 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
}); });
} }
public static void CanHide(Schema schema, HideField command) public static void CanHide(HideField command, Schema schema)
{ {
Guard.NotNull(command); Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (field.IsHidden) if (!field.IsForApi(true))
{
throw new DomainException("Schema field is already hidden.");
}
if (!field.IsForApi())
{ {
throw new DomainException("UI field cannot be hidden."); throw new DomainException("UI field cannot be hidden.");
} }
} }
public static void CanShow(Schema schema, ShowField command) public static void CanDisable(DisableField command, Schema schema)
{ {
Guard.NotNull(command); Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (!field.IsHidden) if (!field.IsForApi(true))
{ {
throw new DomainException("Schema field is already visible."); throw new DomainException("UI field cannot be diabled.");
} }
} }
public static void CanDisable(Schema schema, DisableField command) public static void CanShow(ShowField command, Schema schema)
{ {
Guard.NotNull(command); Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (field.IsDisabled)
{
throw new DomainException("Schema field is already disabled.");
}
if (!field.IsForApi(true)) if (!field.IsForApi(true))
{ {
throw new DomainException("UI field cannot be disabled."); throw new DomainException("UI field cannot be shown.");
} }
} }
public static void CanDelete(Schema schema, DeleteField command) public static void CanEnable(EnableField command, Schema schema)
{ {
Guard.NotNull(command); Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (field.IsLocked) if (!field.IsForApi(true))
{ {
throw new DomainException("Schema field is locked."); throw new DomainException("UI field cannot be enabled.");
} }
} }
public static void CanEnable(Schema schema, EnableField command) public static void CanDelete(DeleteField command, Schema schema)
{ {
Guard.NotNull(command); Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (!field.IsDisabled)
{
throw new DomainException("Schema field is already enabled.");
}
} }
public static void CanLock(Schema schema, LockField command) public static void CanLock(LockField command, Schema schema)
{ {
Guard.NotNull(command); Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, true);
if (field.IsLocked)
{
throw new DomainException("Schema field is already locked.");
}
} }
} }
} }

42
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -17,7 +17,6 @@ using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -27,14 +26,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{ {
public sealed class SchemaGrain : DomainObjectGrain<SchemaState>, ISchemaGrain public sealed class SchemaGrain : DomainObjectGrain<SchemaState>, ISchemaGrain
{ {
private readonly IJsonSerializer serializer; public SchemaGrain(IStore<Guid> store, ISemanticLog log)
public SchemaGrain(IStore<Guid> store, ISemanticLog log, IJsonSerializer serializer)
: base(store, log) : base(store, log)
{ {
Guard.NotNull(serializer);
this.serializer = serializer;
} }
protected override Task<object?> ExecuteAsync(IAggregateCommand command) protected override Task<object?> ExecuteAsync(IAggregateCommand command)
@ -46,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case AddField addField: case AddField addField:
return UpdateReturn(addField, c => return UpdateReturn(addField, c =>
{ {
GuardSchemaField.CanAdd(Snapshot.SchemaDef, c); GuardSchemaField.CanAdd(c, Snapshot.SchemaDef);
Add(c); Add(c);
@ -87,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case DeleteField deleteField: case DeleteField deleteField:
return UpdateReturn(deleteField, c => return UpdateReturn(deleteField, c =>
{ {
GuardSchemaField.CanDelete(Snapshot.SchemaDef, deleteField); GuardSchemaField.CanDelete(deleteField, Snapshot.SchemaDef);
DeleteField(c); DeleteField(c);
@ -97,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case LockField lockField: case LockField lockField:
return UpdateReturn(lockField, c => return UpdateReturn(lockField, c =>
{ {
GuardSchemaField.CanLock(Snapshot.SchemaDef, lockField); GuardSchemaField.CanLock(lockField, Snapshot.SchemaDef);
LockField(c); LockField(c);
@ -107,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case HideField hideField: case HideField hideField:
return UpdateReturn(hideField, c => return UpdateReturn(hideField, c =>
{ {
GuardSchemaField.CanHide(Snapshot.SchemaDef, c); GuardSchemaField.CanHide(c, Snapshot.SchemaDef);
HideField(c); HideField(c);
@ -117,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case ShowField showField: case ShowField showField:
return UpdateReturn(showField, c => return UpdateReturn(showField, c =>
{ {
GuardSchemaField.CanShow(Snapshot.SchemaDef, c); GuardSchemaField.CanShow(c, Snapshot.SchemaDef);
ShowField(c); ShowField(c);
@ -127,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case DisableField disableField: case DisableField disableField:
return UpdateReturn(disableField, c => return UpdateReturn(disableField, c =>
{ {
GuardSchemaField.CanDisable(Snapshot.SchemaDef, c); GuardSchemaField.CanDisable(c, Snapshot.SchemaDef);
DisableField(c); DisableField(c);
@ -137,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case EnableField enableField: case EnableField enableField:
return UpdateReturn(enableField, c => return UpdateReturn(enableField, c =>
{ {
GuardSchemaField.CanEnable(Snapshot.SchemaDef, c); GuardSchemaField.CanEnable(c, Snapshot.SchemaDef);
EnableField(c); EnableField(c);
@ -147,7 +141,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case UpdateField updateField: case UpdateField updateField:
return UpdateReturn(updateField, c => return UpdateReturn(updateField, c =>
{ {
GuardSchemaField.CanUpdate(Snapshot.SchemaDef, c); GuardSchemaField.CanUpdate(c, Snapshot.SchemaDef);
UpdateField(c); UpdateField(c);
@ -157,7 +151,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case ReorderFields reorderFields: case ReorderFields reorderFields:
return UpdateReturn(reorderFields, c => return UpdateReturn(reorderFields, c =>
{ {
GuardSchema.CanReorder(Snapshot.SchemaDef, c); GuardSchema.CanReorder(c, Snapshot.SchemaDef);
Reorder(c); Reorder(c);
@ -167,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case UpdateSchema updateSchema: case UpdateSchema updateSchema:
return UpdateReturn(updateSchema, c => return UpdateReturn(updateSchema, c =>
{ {
GuardSchema.CanUpdate(Snapshot.SchemaDef, c); GuardSchema.CanUpdate(c);
Update(c); Update(c);
@ -177,7 +171,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case PublishSchema publishSchema: case PublishSchema publishSchema:
return UpdateReturn(publishSchema, c => return UpdateReturn(publishSchema, c =>
{ {
GuardSchema.CanPublish(Snapshot.SchemaDef, c); GuardSchema.CanPublish(c);
Publish(c); Publish(c);
@ -187,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case UnpublishSchema unpublishSchema: case UnpublishSchema unpublishSchema:
return UpdateReturn(unpublishSchema, c => return UpdateReturn(unpublishSchema, c =>
{ {
GuardSchema.CanUnpublish(Snapshot.SchemaDef, c); GuardSchema.CanUnpublish(c);
Unpublish(c); Unpublish(c);
@ -197,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case ConfigureScripts configureScripts: case ConfigureScripts configureScripts:
return UpdateReturn(configureScripts, c => return UpdateReturn(configureScripts, c =>
{ {
GuardSchema.CanConfigureScripts(Snapshot.SchemaDef, c); GuardSchema.CanConfigureScripts(c);
ConfigureScripts(c); ConfigureScripts(c);
@ -207,7 +201,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case ChangeCategory changeCategory: case ChangeCategory changeCategory:
return UpdateReturn(changeCategory, c => return UpdateReturn(changeCategory, c =>
{ {
GuardSchema.CanChangeCategory(Snapshot.SchemaDef, c); GuardSchema.CanChangeCategory(c);
ChangeCategory(c); ChangeCategory(c);
@ -227,7 +221,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case ConfigureUIFields configureUIFields: case ConfigureUIFields configureUIFields:
return UpdateReturn(configureUIFields, c => return UpdateReturn(configureUIFields, c =>
{ {
GuardSchema.CanConfigureUIFields(Snapshot.SchemaDef, c); GuardSchema.CanConfigureUIFields(c, Snapshot.SchemaDef);
ConfigureUIFields(c); ConfigureUIFields(c);
@ -237,7 +231,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case DeleteSchema deleteSchema: case DeleteSchema deleteSchema:
return Update(deleteSchema, c => return Update(deleteSchema, c =>
{ {
GuardSchema.CanDelete(Snapshot.SchemaDef, c); GuardSchema.CanDelete(c);
Delete(c); Delete(c);
}); });
@ -258,7 +252,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var schemaSource = Snapshot.SchemaDef; var schemaSource = Snapshot.SchemaDef;
var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton); var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton);
var events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options); var events = schemaSource.Synchronize(schemaTarget, () => Snapshot.SchemaFieldsTotal + 1, options);
foreach (var @event in events) foreach (var @event in events)
{ {

15
backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs

@ -19,7 +19,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Schemas.State namespace Squidex.Domain.Apps.Entities.Schemas.State
{ {
[CollectionName("Schemas")] [CollectionName("Schemas")]
public class SchemaState : DomainObjectState<SchemaState>, ISchemaEntity public sealed class SchemaState : DomainObjectState<SchemaState>, ISchemaEntity
{ {
[DataMember] [DataMember]
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }
@ -33,8 +33,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State
[DataMember] [DataMember]
public Schema SchemaDef { get; set; } public Schema SchemaDef { get; set; }
public void ApplyEvent(IEvent @event) public override bool ApplyEvent(IEvent @event)
{ {
var previousSchema = SchemaDef;
switch (@event) switch (@event)
{ {
case SchemaCreated e: case SchemaCreated e:
@ -44,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State
AppId = e.AppId; AppId = e.AppId;
break; return true;
} }
case FieldAdded e: case FieldAdded e:
@ -187,14 +189,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State
{ {
IsDeleted = true; IsDeleted = true;
break; return true;
}
} }
} }
public override SchemaState Apply(Envelope<IEvent> @event) return !ReferenceEquals(previousSchema, SchemaDef);
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
} }
} }
} }

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

@ -31,6 +31,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.7.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.7.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="taglib-sharp-netstandard2.0" Version="2.1.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>

3
backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets namespace Squidex.Domain.Apps.Events.Assets
@ -17,6 +18,8 @@ namespace Squidex.Domain.Apps.Events.Assets
public string Slug { get; set; } public string Slug { get; set; }
public AssetMetadata? Metadata { get; set; }
public HashSet<string>? Tags { get; set; } public HashSet<string>? Tags { get; set; }
} }
} }

9
backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs

@ -7,11 +7,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets namespace Squidex.Domain.Apps.Events.Assets
{ {
[EventType(nameof(AssetCreated))] [EventType(nameof(AssetCreated), 2)]
public sealed class AssetCreated : AssetEvent public sealed class AssetCreated : AssetEvent
{ {
public Guid ParentId { get; set; } public Guid ParentId { get; set; }
@ -28,11 +29,9 @@ namespace Squidex.Domain.Apps.Events.Assets
public long FileSize { get; set; } public long FileSize { get; set; }
public bool IsImage { get; set; } public AssetType Type { get; set; }
public int? PixelWidth { get; set; } public AssetMetadata Metadata { get; set; }
public int? PixelHeight { get; set; }
public HashSet<string>? Tags { get; set; } public HashSet<string>? Tags { get; set; }
} }

11
backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs

@ -5,11 +5,12 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure.Reflection; using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets namespace Squidex.Domain.Apps.Events.Assets
{ {
[TypeName("AssetUpdated")] [EventType(nameof(AssetUpdated), 2)]
public sealed class AssetUpdated : AssetEvent public sealed class AssetUpdated : AssetEvent
{ {
public string MimeType { get; set; } public string MimeType { get; set; }
@ -20,10 +21,8 @@ namespace Squidex.Domain.Apps.Events.Assets
public long FileVersion { get; set; } public long FileVersion { get; set; }
public bool IsImage { get; set; } public AssetType Type { get; set; }
public int? PixelWidth { get; set; } public AssetMetadata Metadata { get; set; }
public int? PixelHeight { get; set; }
} }
} }

12
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Globalization;
using MongoDB.Bson.IO; using MongoDB.Bson.IO;
using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter; using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter;
@ -142,19 +141,12 @@ namespace Squidex.Infrastructure.MongoDb
public override void WriteValue(DateTime value) public override void WriteValue(DateTime value)
{ {
bsonWriter.WriteString(value.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); bsonWriter.WriteString(value.ToIso8601());
} }
public override void WriteValue(DateTimeOffset value) public override void WriteValue(DateTimeOffset value)
{ {
if (value.Offset == TimeSpan.Zero) bsonWriter.WriteString(value.UtcDateTime.ToIso8601());
{
bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture));
}
else
{
bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture));
}
} }
public override void WriteValue(byte[]? value) public override void WriteValue(byte[]? value)

1
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.Linq; using System.Linq;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;

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

@ -18,6 +18,11 @@ namespace Squidex.Infrastructure
return source.Intersect(other).Count() == other.Count; return source.Intersect(other).Count() == other.Count;
} }
public static bool SetEquals<T>(this ICollection<T> source, ICollection<T> other, IEqualityComparer<T> comparer)
{
return source.Intersect(other, comparer).Count() == other.Count;
}
public static IResultList<T> SortSet<T, TKey>(this IResultList<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where T : class public static IResultList<T> SortSet<T, TKey>(this IResultList<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where T : class
{ {
return ResultList.Create(input.Total, SortList(input, idProvider, ids)); return ResultList.Create(input.Total, SortList(input, idProvider, ids));
@ -162,9 +167,19 @@ namespace Squidex.Infrastructure
public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue> other, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer) where TKey : notnull public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue> other, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer) where TKey : notnull
{ {
if (other == null)
{
return false;
}
if (dictionary.Count != other.Count)
{
return false;
}
var comparer = new KeyValuePairComparer<TKey, TValue>(keyComparer, valueComparer); var comparer = new KeyValuePairComparer<TKey, TValue>(keyComparer, valueComparer);
return other != null && dictionary.Count == other.Count && !dictionary.Except(other, comparer).Any(); return !dictionary.Except(other, comparer).Any();
} }
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull

118
backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs

@ -11,12 +11,14 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
#pragma warning disable IDE0044 // Add readonly modifier
namespace Squidex.Infrastructure.Collections namespace Squidex.Infrastructure.Collections
{ {
public class ArrayDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue> where TKey : notnull public class ArrayDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue> where TKey : notnull
{ {
private readonly IEqualityComparer<TKey> keyComparer; private readonly IEqualityComparer<TKey> keyComparer;
private readonly KeyValuePair<TKey, TValue>[] items; private KeyValuePair<TKey, TValue>[] items;
public TValue this[TKey key] public TValue this[TKey key]
{ {
@ -66,85 +68,131 @@ namespace Squidex.Infrastructure.Collections
this.keyComparer = keyComparer; this.keyComparer = keyComparer;
} }
public KeyValuePair<TKey, TValue>[] With(TKey key, TValue value) public bool IsUnchanged(KeyValuePair<TKey, TValue>[] values)
{ {
var result = new List<KeyValuePair<TKey, TValue>>(Math.Max(items.Length, 1)); return ReferenceEquals(values, items);
}
var wasReplaced = false; public ArrayDictionary<TKey, TValue> With(TKey key, TValue value, IEqualityComparer<TValue>? valueComparer = null)
{
return With<ArrayDictionary<TKey, TValue>>(key, value, valueComparer);
}
for (var i = 0; i < items.Length; i++) public TArray With<TArray>(TKey key, TValue value, IEqualityComparer<TValue>? valueComparer = null) where TArray : ArrayDictionary<TKey, TValue>
{ {
var item = items[i]; var index = IndexOf(key);
if (wasReplaced || !keyComparer.Equals(item.Key, key)) if (index < 0)
{ {
result.Add(item); var result = new KeyValuePair<TKey, TValue>[items.Length + 1];
Array.Copy(items, 0, result, 0, items.Length);
result[^1] = new KeyValuePair<TKey, TValue>(key, value);
return Create<TArray>(result);
} }
else
var existing = items[index].Value;
if (valueComparer == null || !valueComparer.Equals(value, existing))
{ {
result.Add(new KeyValuePair<TKey, TValue>(key, value)); var result = new KeyValuePair<TKey, TValue>[items.Length];
wasReplaced = true;
Array.Copy(items, 0, result, 0, items.Length);
result[index] = new KeyValuePair<TKey, TValue>(key, value);
return Create<TArray>(result);
} }
return Self<TArray>();
} }
if (!wasReplaced) public ArrayDictionary<TKey, TValue> Without(TKey key)
{ {
result.Add(new KeyValuePair<TKey, TValue>(key, value)); return Without<ArrayDictionary<TKey, TValue>>(key);
} }
return result.ToArray(); public TArray Without<TArray>(TKey key) where TArray : ArrayDictionary<TKey, TValue>
} {
var index = IndexOf(key);
public KeyValuePair<TKey, TValue>[] Without(TKey key) if (index < 0)
{ {
var result = new List<KeyValuePair<TKey, TValue>>(Math.Max(items.Length, 1)); return Self<TArray>();
}
var wasRemoved = false; var result = Array.Empty<KeyValuePair<TKey, TValue>>();
for (var i = 0; i < items.Length; i++) if (items.Length > 1)
{ {
var item = items[i]; result = new KeyValuePair<TKey, TValue>[items.Length - 1];
if (wasRemoved || !keyComparer.Equals(item.Key, key)) var afterIndex = items.Length - index - 1;
{
result.Add(item); Array.Copy(items, 0, result, 0, index);
Array.Copy(items, index, result, index, afterIndex);
} }
else
return Create<TArray>(result);
}
private TArray Self<TArray>() where TArray : ArrayDictionary<TKey, TValue>
{ {
wasRemoved = true; return (this as TArray)!;
} }
private TArray Create<TArray>(KeyValuePair<TKey, TValue>[] newItems) where TArray : ArrayDictionary<TKey, TValue>
{
if (ReferenceEquals(items, newItems))
{
return Self<TArray>();
} }
return result.ToArray(); var newClone = (TArray)MemberwiseClone();
newClone.items = newItems;
return newClone;
} }
public bool ContainsKey(TKey key) public bool ContainsKey(TKey key)
{ {
for (var i = 0; i < items.Length; i++) var index = IndexOf(key);
return index >= 0;
}
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{ {
if (keyComparer.Equals(items[i].Key, key)) var index = IndexOf(key);
if (index >= 0)
{ {
value = items[index].Value;
return true; return true;
} }
} else
{
value = default!;
return false; return false;
} }
}
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) private int IndexOf(TKey key)
{ {
for (var i = 0; i < items.Length; i++) for (var i = 0; i < items.Length; i++)
{ {
if (keyComparer.Equals(items[i].Key, key)) if (keyComparer.Equals(items[i].Key, key))
{ {
value = items[i].Value; return i;
return true;
} }
} }
value = default!; return -1;
return false;
} }
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()

15
backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs

@ -32,12 +32,21 @@ namespace Squidex.Infrastructure.Commands
this.store = store; this.store = store;
} }
protected sealed override void ApplyEvent(Envelope<IEvent> @event) protected sealed override bool ApplyEvent(Envelope<IEvent> @event, bool isLoading)
{ {
var newVersion = Version + 1; var newVersion = Version + 1;
snapshot = OnEvent(@event); var newSnapshot = OnEvent(@event);
if (!ReferenceEquals(Snapshot, newSnapshot) || isLoading)
{
snapshot = newSnapshot;
snapshot.Version = newVersion; snapshot.Version = newVersion;
return true;
}
return false;
} }
protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion)
@ -47,7 +56,7 @@ namespace Squidex.Infrastructure.Commands
protected sealed override Task ReadAsync(Type type, Guid id) protected sealed override Task ReadAsync(Type type, Guid id)
{ {
persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot<T>(ApplySnapshot), ApplyEvent); persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot<T>(ApplySnapshot), x => ApplyEvent(x, true));
return persistence.ReadAsync(); return persistence.ReadAsync();
} }

7
backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs

@ -73,10 +73,11 @@ namespace Squidex.Infrastructure.Commands
@event.SetAggregateId(id); @event.SetAggregateId(id);
ApplyEvent(@event); if (ApplyEvent(@event, false))
{
uncomittedEvents.Add(@event); uncomittedEvents.Add(@event);
} }
}
public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents() public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents()
{ {
@ -206,7 +207,7 @@ namespace Squidex.Infrastructure.Commands
protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion); protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion);
protected abstract void ApplyEvent(Envelope<IEvent> @event); protected abstract bool ApplyEvent(Envelope<IEvent> @event, bool isLoading);
protected abstract Task ReadAsync(Type type, Guid id); protected abstract Task ReadAsync(Type type, Guid id);

15
backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs

@ -54,17 +54,26 @@ namespace Squidex.Infrastructure.Commands
return default!; return default!;
} }
protected sealed override void ApplyEvent(Envelope<IEvent> @event) protected sealed override bool ApplyEvent(Envelope<IEvent> @event, bool isLoading)
{ {
var newVersion = Version + 1;
var snapshot = OnEvent(@event); var snapshot = OnEvent(@event);
snapshot.Version = Version + 1; if (!ReferenceEquals(Snapshot, snapshot) || isLoading)
{
snapshot.Version = newVersion;
snapshots.Add(snapshot); snapshots.Add(snapshot);
return true;
}
return false;
} }
protected sealed override Task ReadAsync(Type type, Guid id) protected sealed override Task ReadAsync(Type type, Guid id)
{ {
persistence = store.WithEventSourcing(type, id, ApplyEvent); persistence = store.WithEventSourcing(type, id, x => ApplyEvent(x, true));
return persistence.ReadAsync(); return persistence.ReadAsync();
} }

3
backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs

@ -7,7 +7,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json; using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
@ -103,7 +102,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
case JsonToken.Boolean: case JsonToken.Boolean:
return JsonValue.Create((bool)reader.Value!); return JsonValue.Create((bool)reader.Value!);
case JsonToken.Date: case JsonToken.Date:
return JsonValue.Create(((DateTime)reader.Value!).ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); return JsonValue.Create(((DateTime)reader.Value!).ToIso8601());
case JsonToken.String: case JsonToken.String:
return JsonValue.Create(reader.Value!.ToString()); return JsonValue.Create(reader.Value!.ToString());
case JsonToken.Null: case JsonToken.Null:

3
backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Diagnostics.CodeAnalysis;
namespace Squidex.Infrastructure.Json.Objects namespace Squidex.Infrastructure.Json.Objects
{ {
@ -13,6 +14,8 @@ namespace Squidex.Infrastructure.Json.Objects
{ {
JsonValueType Type { get; } JsonValueType Type { get; }
bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result);
string ToJsonString(); string ToJsonString();
string ToString(); string ToString();

18
backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs

@ -8,6 +8,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq; using System.Linq;
namespace Squidex.Infrastructure.Json.Objects namespace Squidex.Infrastructure.Json.Objects
@ -92,5 +94,21 @@ namespace Squidex.Infrastructure.Json.Objects
{ {
return $"[{string.Join(", ", this.Select(x => x.ToJsonString()))}]"; return $"[{string.Join(", ", this.Select(x => x.ToJsonString()))}]";
} }
public bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result)
{
Guard.NotNull(pathSegment);
if (pathSegment != null && int.TryParse(pathSegment, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index) && index >= 0 && index < Count)
{
result = this[index];
return true;
}
result = null!;
return false;
}
} }
} }

8
backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Diagnostics.CodeAnalysis;
namespace Squidex.Infrastructure.Json.Objects namespace Squidex.Infrastructure.Json.Objects
{ {
@ -51,5 +52,12 @@ namespace Squidex.Infrastructure.Json.Objects
{ {
return "null"; return "null";
} }
public bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result)
{
result = null!;
return false;
}
} }
} }

9
backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs

@ -51,7 +51,7 @@ namespace Squidex.Infrastructure.Json.Objects
get { return JsonValueType.Array; } get { return JsonValueType.Array; }
} }
internal JsonObject() public JsonObject()
{ {
inner = new Dictionary<string, IJsonValue>(); inner = new Dictionary<string, IJsonValue>();
} }
@ -132,5 +132,12 @@ namespace Squidex.Infrastructure.Json.Objects
{ {
return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value.ToJsonString()}"))}}}"; return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value.ToJsonString()}"))}}}";
} }
public bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result)
{
Guard.NotNull(pathSegment);
return TryGetValue(pathSegment, out result!);
}
} }
} }

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

Loading…
Cancel
Save