From 4e25ccc315c490135479167cac7c95d50554fc87 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 7 Jan 2020 09:32:02 +0100 Subject: [PATCH] Asset metadata. (#467) * Asset metadata. * Dynamic queries. * File tag support. * Idempotent APIs --- .../Notification/NotificationAction.cs | 1 - .../Notification/NotificationPlugin.cs | 1 - .../Apps/AppClients.cs | 10 +- .../Apps/AppContributors.cs | 4 +- .../Apps/AppPatterns.cs | 6 +- .../Apps/LanguagesConfig.cs | 17 +- .../Apps/Roles.cs | 11 +- .../Assets/AssetMetadata.cs | 110 ++++++++ .../Assets/AssetType.cs} | 11 +- .../Contents/Workflows.cs | 10 +- .../DeepComparer.cs | 31 +++ .../Rules/Rule.cs | 48 +++- .../Rules/RuleAction.cs | 6 + .../Rules/RuleTrigger.cs | 7 + .../Schemas/FieldCollection.cs | 5 + .../Schemas/FieldNames.cs | 6 + .../Schemas/FieldProperties.cs | 6 + .../Schemas/NestedField.cs | 25 ++ .../Schemas/NestedField{T}.cs | 7 + .../Schemas/RootField.cs | 25 ++ .../Schemas/RootField{T}.cs | 5 + .../Schemas/Schema.cs | 70 ++++- .../Schemas/SchemaProperties.cs | 6 + .../Schemas/SchemaScripts.cs | 7 + .../Squidex.Domain.Apps.Core.Model.csproj | 1 + .../SchemaSynchronizer.cs | 15 +- .../EventSynchronization/SyncHelpers.cs | 9 - .../GenerateEdmSchema/EdmTypeVisitor.cs | 3 +- .../GenerateJsonSchema/Builder.cs | 31 ++- .../GenerateJsonSchema/JsonTypeVisitor.cs | 2 +- .../HandleRules/RuleEventFormatter.cs | 19 +- .../ValidateContent/IAssetInfo.cs | 11 +- .../Validators/AssetsValidator.cs | 13 +- .../Assets/MongoAssetEntity.cs | 19 +- .../Assets/Visitors/FindExtensions.cs | 6 +- .../Visitors/FirstPascalPathConverter.cs | 30 +++ .../Visitors/FirstPascalPathExtension.cs | 25 ++ .../FullText/MongoIndexOutput.cs | 2 +- .../Apps/Guards/GuardApp.cs | 6 - .../Apps/Guards/GuardAppContributors.cs | 9 +- .../Apps/State/AppState.cs | 225 ++++++++-------- .../Assets/AssetCommandMiddleware.cs | 36 +-- .../Assets/AssetEntity.cs | 9 +- .../Assets/AssetFolderGrain.cs | 2 +- .../Assets/AssetGrain.cs | 10 +- .../Assets/Commands/AnnotateAsset.cs | 3 + .../Assets/Commands/CreateAsset.cs | 2 +- .../Assets/Commands/UploadAssetCommand.cs | 5 +- .../Assets/FileTagAssetMetadataSource.cs | 222 ++++++++++++++++ .../Assets/FileTypeTagGenerator.cs | 25 +- .../Assets/Guards/GuardAsset.cs | 17 +- .../Assets/Guards/GuardAssetFolder.cs | 12 +- .../Assets/IAssetMetadataSource.cs | 20 ++ .../Assets/IEnrichedAssetEntity.cs | 2 + .../Assets/ImageMetadataSource.cs | 69 +++++ .../Assets/ImageTagGenerator.cs | 39 --- .../Assets/Queries/AssetEnricher.cs | 43 ++- .../Assets/Queries/AssetQueryParser.cs | 34 ++- .../Assets/State/AssetFolderState.cs | 23 +- .../Assets/State/AssetState.cs | 50 ++-- .../Contents/GraphQL/GraphQLModel.cs | 2 +- .../Contents/GraphQL/IGraphModel.cs | 2 +- .../Contents/GraphQL/Types/AllTypes.cs | 19 +- .../GraphQL/Types/AppQueriesGraphType.cs | 4 +- .../Contents/GraphQL/Types/AssetGraphType.cs | 53 +++- .../GraphQL/Types/ContentDataFlatGraphType.cs | 3 +- .../GraphQL/Types/ContentDataGraphType.cs | 3 +- .../Contents/GraphQL/Types/NestedGraphType.cs | 3 +- .../GraphQL/Types/QueryGraphTypeVisitor.cs | 61 +++-- .../GraphQL/Types/Utils/JsonGraphType.cs | 2 +- .../Contents/Queries/ContentEnricher.cs | 3 +- .../Contents/State/ContentState.cs | 9 +- ...xtensions.cs => DomainEntityExtensions.cs} | 2 +- .../DomainObjectState.cs | 43 ++- .../EntityMapper.cs | 82 ------ .../IUpdateableEntity.cs | 21 -- .../IUpdateableEntityWithCreatedBy.cs | 16 -- .../IUpdateableEntityWithLastModifiedBy.cs | 16 -- .../Rules/Guards/GuardRule.cs | 17 +- .../Rules/RuleGrain.cs | 6 +- .../Rules/State/RuleState.cs | 15 +- .../Schemas/Guards/GuardSchema.cs | 34 +-- .../Schemas/Guards/GuardSchemaField.cs | 52 ++-- .../Schemas/SchemaGrain.cs | 42 ++- .../Schemas/State/SchemaState.cs | 15 +- .../Squidex.Domain.Apps.Entities.csproj | 1 + .../Assets/AssetAnnotated.cs | 3 + .../Assets/AssetCreated.cs | 9 +- .../Assets/AssetUpdated.cs | 11 +- .../MongoDb/BsonJsonWriter.cs | 12 +- .../MongoDb/Queries/SortBuilder.cs | 1 - .../CollectionExtensions.cs | 17 +- .../ArrayDictionary{TKey,TValue}.cs | 136 +++++++--- .../Commands/DomainObjectGrain.cs | 17 +- .../Commands/DomainObjectGrainBase.cs | 9 +- .../Commands/LogSnapshotDomainObjectGrain.cs | 17 +- .../Json/Newtonsoft/JsonValueConverter.cs | 3 +- .../Json/Objects/IJsonValue.cs | 3 + .../Json/Objects/JsonArray.cs | 18 ++ .../Json/Objects/JsonNull.cs | 8 + .../Json/Objects/JsonObject.cs | 9 +- .../Json/Objects/JsonScalar.cs | 8 + .../Json/Objects/JsonValue.cs | 27 ++ .../Queries/ClrValue.cs | 5 + .../Queries/ClrValueType.cs | 1 + .../Queries/Json/OperatorValidator.cs | 2 + .../Queries/Json/PropertyPathValidator.cs | 5 + .../Queries/Json/ValueConverter.cs | 63 +++++ .../Queries/OData/PropertyPathVisitor.cs | 5 + .../StringExtensions.cs | 6 + .../Squidex.Infrastructure/Validation/Not.cs | 6 - .../src/Squidex.Web/Services/UrlGenerator.cs | 3 +- .../Api/Config/OpenApi/OpenApiServices.cs | 30 ++- .../Apps/AppContributorsController.cs | 3 +- .../Assets/AssetContentController.cs | 3 +- .../Controllers/Assets/AssetsController.cs | 2 +- .../Assets/Models/AnnotateAssetDto.cs | 6 + .../Api/Controllers/Assets/Models/AssetDto.cs | 62 ++++- .../Models/{AssetMetadata.cs => AssetMeta.cs} | 2 +- .../Api/Controllers/Rules/RulesController.cs | 2 - .../Schemas/SchemaFieldsController.cs | 10 - .../Controllers/Schemas/SchemasController.cs | 2 - .../Squidex/Config/Domain/AssetServices.cs | 8 +- .../Squidex/Config/Domain/CommandsServices.cs | 1 - .../Model/Apps/AppClientsTests.cs | 10 +- .../Model/Apps/AppContributorsTests.cs | 11 +- .../Model/Apps/AppPatternJsonTests.cs | 3 +- .../Model/Apps/AppPatternsTests.cs | 12 +- .../Model/Apps/LanguagesConfigTests.cs | 18 +- .../Model/Apps/RolesTests.cs | 10 +- .../Model/Assets/AssetMetadataTests.cs | 170 ++++++++++++ .../Model/Rules/RuleTests.cs | 43 ++- .../Model/Schemas/ArrayFieldTests.cs | 32 ++- .../Model/Schemas/SchemaTests.cs | 95 +++++-- .../SchemaSynchronizerTests.cs | 62 +++-- .../ValidateContent/AssetsFieldTests.cs | 25 +- .../Apps/AppGrainTests.cs | 159 ++++++----- .../Apps/Guards/GuardAppContributorsTests.cs | 5 +- .../Apps/Guards/GuardAppTests.cs | 5 +- .../Assets/AssetCommandMiddlewareTests.cs | 21 +- .../Assets/AssetFolderGrainTests.cs | 49 ++-- .../Assets/AssetGrainTests.cs | 108 +++++--- .../Assets/FileTypeTagGeneratorTests.cs | 23 +- .../Assets/Guards/GuardAssetFolderTests.cs | 28 +- .../Assets/Guards/GuardAssetTests.cs | 11 +- .../Assets/ImageMetadataSourceTests.cs | 114 ++++++++ .../Assets/ImageTagGeneratorTests.cs | 72 ----- .../Assets/MongoDb/MongoDbQueryTests.cs | 17 +- .../Assets/Queries/AssetEnricherTests.cs | 40 ++- .../Contents/GraphQL/GraphQLQueriesTests.cs | 42 ++- .../Contents/GraphQL/GraphQLTestBase.cs | 10 +- .../Queries/ContentEnricherAssetsTests.cs | 21 +- .../Contents/Text/TextIndexerBenchmark.cs | 2 + .../Rules/Guards/GuardRuleTests.cs | 39 +-- .../Rules/RuleGrainTests.cs | 65 +++-- .../Schemas/Guards/GuardSchemaFieldTests.cs | 187 ++++++------- .../Schemas/Guards/GuardSchemaTests.cs | 50 ++-- .../Schemas/SchemaGrainTests.cs | 164 +++++++----- .../TestHelpers/AExtensions.cs | 12 + .../TestHelpers/AssertHelper.cs | 5 + .../Commands/DomainObjectGrainTests.cs | 19 ++ .../LogSnapshotDomainObjectGrainTests.cs | 19 ++ .../Json/Objects/JsonObjectTests.cs | 120 ++++++++- .../Queries/JsonQueryConversionTests.cs | 2 +- .../Queries/QueryJsonConversionTests.cs | 251 ++++++++++++------ .../Queries/QueryODataConversionTests.cs | 19 +- .../TestHelpers/MyDomainObject.cs | 8 +- .../TestHelpers/MyDomainState.cs | 15 +- backend/tools/Migrate_01/MigrationPath.cs | 6 +- .../Migrate_01/OldEvents/AssetCreated.cs | 62 +++++ .../Migrate_01/OldEvents/AssetUpdated.cs | 51 ++++ .../pages/restore/restore-page.component.html | 2 +- .../pages/users/user-page.component.ts | 4 +- .../administration/state/users.forms.ts | 14 +- .../schemas/pages/schema/field.component.ts | 4 - .../schema/schema-edit-form.component.ts | 4 - .../schema/schema-export-form.component.ts | 14 +- .../schema-preview-urls-form.component.ts | 8 +- .../schema/schema-scripts-form.component.ts | 4 - .../pages/schema/schema-ui-form.component.ts | 12 +- .../pages/schemas/schema-form.component.ts | 6 +- .../clients/client-add-form.component.ts | 6 +- .../settings/pages/roles/role.component.ts | 10 +- .../framework/angular/pipes/numbers.pipes.ts | 17 +- frontend/app/framework/state.ts | 18 +- .../components/asset-dialog.component.html | 34 +++ .../components/asset-dialog.component.ts | 2 + .../components/asset-folder-form.component.ts | 8 +- .../shared/components/asset.component.html | 4 +- .../app/shared/services/apps.service.spec.ts | 2 +- .../shared/services/assets.service.spec.ts | 20 +- .../app/shared/services/assets.service.ts | 17 +- .../shared/services/contents.service.spec.ts | 2 +- .../app/shared/services/rules.service.spec.ts | 2 +- .../shared/services/schemas.service.spec.ts | 4 +- .../app/shared/services/schemas.service.ts | 1 + frontend/app/shared/state/apps.forms.ts | 10 +- .../app/shared/state/assets.forms.spec.ts | 106 ++++++++ frontend/app/shared/state/assets.forms.ts | 150 +++++++++-- frontend/app/shared/state/backups.forms.ts | 6 +- frontend/app/shared/state/clients.forms.ts | 14 +- frontend/app/shared/state/comments.form.ts | 4 +- frontend/app/shared/state/contents.forms.ts | 4 +- frontend/app/shared/state/contents.state.ts | 30 +-- .../app/shared/state/contributors.forms.ts | 8 +- frontend/app/shared/state/languages.forms.ts | 8 +- frontend/app/shared/state/patterns.forms.ts | 4 +- frontend/app/shared/state/roles.forms.ts | 24 +- frontend/app/shared/state/schemas.forms.ts | 111 +++++--- frontend/app/shared/state/schemas.state.ts | 53 ++-- frontend/app/shared/state/workflows.forms.ts | 4 +- frontend/app/theme/_bootstrap.scss | 2 +- 212 files changed, 3824 insertions(+), 1790 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs rename backend/src/{Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs => Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs} (65%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/DeepComparer.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathConverter.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathExtension.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/ImageMetadataSource.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs rename backend/src/Squidex.Domain.Apps.Entities/{EntityExtensions.cs => DomainEntityExtensions.cs} (93%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs rename backend/src/Squidex/Areas/Api/Controllers/Assets/Models/{AssetMetadata.cs => AssetMeta.cs} (93%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageMetadataSourceTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs create mode 100644 backend/tools/Migrate_01/OldEvents/AssetCreated.cs create mode 100644 backend/tools/Migrate_01/OldEvents/AssetUpdated.cs create mode 100644 frontend/app/shared/state/assets.forms.spec.ts diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs index e54e8d851..839d24d35 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.ComponentModel.DataAnnotations; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs index d0b000911..7a3006b27 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs index f9d7c83a0..b96e3a80a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.Apps { Guard.NotNullOrEmpty(id); - return new AppClients(Without(id)); + return Without(id); } [Pure] @@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Core.Apps throw new ArgumentException("Id already exists.", nameof(id)); } - return new AppClients(With(id, client)); + return With(id, client); } [Pure] @@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Core.Apps throw new ArgumentException("Id already exists.", nameof(id)); } - return new AppClients(With(id, new AppClient(id, secret, Role.Editor))); + return With(id, new AppClient(id, secret, Role.Editor)); } [Pure] @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return new AppClients(With(id, client.Rename(newName))); + return With(id, client.Rename(newName), DeepComparer.Instance); } [Pure] @@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return new AppClients(With(id, client.Update(role))); + return With(id, client.Update(role), DeepComparer.Instance); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs index 5d0a81cac..d072bf94d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs +++ b/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(role); - return new AppContributors(With(contributorId, role)); + return With(contributorId, role, EqualityComparer.Default); } [Pure] @@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Core.Apps { Guard.NotNullOrEmpty(contributorId); - return new AppContributors(Without(contributorId)); + return Without(contributorId); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs index e31daa5e7..e5114407d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs @@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Apps [Pure] public AppPatterns Remove(Guid id) { - return new AppPatterns(Without(id)); + return Without(id); } [Pure] @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.Apps throw new ArgumentException("Id already exists.", nameof(id)); } - return new AppPatterns(With(id, newPattern)); + return With(id, newPattern); } [Pure] @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return new AppPatterns(With(id, appPattern.Update(name, pattern, message))); + return With(id, appPattern.Update(name, pattern, message), DeepComparer.Instance); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index 069c640ec..88cb22428 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.Apps { Guard.NotNull(language); - return new LanguagesConfig(languages, languages[language]); + return Create(languages, languages[language]); } [Pure] @@ -109,12 +109,11 @@ namespace Squidex.Domain.Apps.Core.Apps { Guard.NotNull(config); - var newLanguages = - new ArrayDictionary(languages.With(config.Language, config)); + var newLanguages = languages.With(config.Language, config); var newMaster = Master?.Language == config.Language ? config : Master; - return new LanguagesConfig(newLanguages, newMaster!); + return Create(newLanguages, newMaster!); } [Pure] @@ -134,6 +133,16 @@ namespace Squidex.Domain.Apps.Core.Apps newLanguages.Values.FirstOrDefault(x => x.Language == Master.Language) ?? newLanguages.Values.FirstOrDefault(); + return Create(newLanguages, newMaster); + } + + private LanguagesConfig Create(ArrayDictionary newLanguages, LanguageConfig newMaster) + { + if (newLanguages.EqualsDictionary(languages, EqualityComparer.Default, DeepComparer.Instance) && newMaster.Language.Equals(master.Language)) + { + return this; + } + return new LanguagesConfig(newLanguages, newMaster); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs index 1eaa3f4de..493aa47ce 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.Apps [Pure] public Roles Remove(string name) { - return new Roles(inner.Without(name)); + return Create(inner.Without(name)); } [Pure] @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return new Roles(inner.With(name, newRole)); + return Create(inner.With(name, newRole)); } [Pure] @@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return new Roles(inner.With(name, role.Update(permissions))); + return Create(inner.With(name, role.Update(permissions), DeepComparer.Instance)); } 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(); } + + private Roles Create(ArrayDictionary newRoles) + { + return ReferenceEquals(inner, newRoles) ? this : new Roles(newRoles); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs new file mode 100644 index 000000000..68842be55 --- /dev/null +++ b/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 + { + 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? 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; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs similarity index 65% rename from backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs index 67d24d64a..7acd36b8d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs @@ -1,14 +1,17 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // 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 } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs index dd18859a3..e6094cada 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.Contents [Pure] public Workflows Remove(Guid id) { - return new Workflows(Without(id)); + return Without(id); } [Pure] @@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.Contents { Guard.NotNullOrEmpty(name); - return new Workflows(With(workflowId, Workflow.CreateDefault(name))); + return With(workflowId, Workflow.CreateDefault(name)); } [Pure] @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Contents { Guard.NotNull(workflow); - return new Workflows(With(Guid.Empty, workflow)); + return With(Guid.Empty, workflow, DeepComparer.Instance); } [Pure] @@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.Contents { Guard.NotNull(workflow); - return new Workflows(With(id, workflow)); + return With(id, workflow, DeepComparer.Instance); } [Pure] @@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Core.Contents return this; } - return new Workflows(With(id, workflow)); + return With(id, workflow, DeepComparer.Instance); } public Workflow GetFirst() diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/DeepComparer.cs b/backend/src/Squidex.Domain.Apps.Core.Model/DeepComparer.cs new file mode 100644 index 000000000..15d1a8f90 --- /dev/null +++ b/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 : IEqualityComparer + { + public static readonly DeepComparer Instance = new DeepComparer(); + + private DeepComparer() + { + } + + public bool Equals(T x, T y) + { + return x.IsDeepEqual(y); + } + + public int GetHashCode(T obj) + { + return 0; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs index c7be01ae6..c3d2073b4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs +++ b/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(action); - this.trigger = trigger; - this.trigger.Freeze(); - - this.action = action; - this.action.Freeze(); + SetTrigger(trigger); + SetAction(action); } [Pure] public Rule Rename(string newName) { + if (string.Equals(name, newName)) + { + return this; + } + return Clone(clone => { clone.name = newName; @@ -62,6 +64,11 @@ namespace Squidex.Domain.Apps.Core.Rules [Pure] public Rule Enable() { + if (isEnabled) + { + return this; + } + return Clone(clone => { clone.isEnabled = true; @@ -71,6 +78,11 @@ namespace Squidex.Domain.Apps.Core.Rules [Pure] public Rule Disable() { + if (!isEnabled) + { + return this; + } + return Clone(clone => { clone.isEnabled = false; @@ -87,11 +99,14 @@ namespace Squidex.Domain.Apps.Core.Rules throw new ArgumentException("New trigger has another type.", nameof(newTrigger)); } - newTrigger.Freeze(); + if (trigger.DeepEquals(newTrigger)) + { + return this; + } 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)); } - newAction.Freeze(); + if (action.DeepEquals(newAction)) + { + return this; + } 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(); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs index 44cb564d9..fe526f783 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using DeepEqual.Syntax; using Squidex.Infrastructure.Validation; namespace Squidex.Domain.Apps.Core.Rules @@ -37,5 +38,10 @@ namespace Squidex.Domain.Apps.Core.Rules { yield break; } + + public bool DeepEquals(RuleAction action) + { + return this.WithDeepEqual(action).IgnoreProperty(x => x.IsFrozen).Compare(); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs index 95c1deb0f..0c95b27be 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs @@ -5,10 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using DeepEqual.Syntax; + namespace Squidex.Domain.Apps.Core.Rules { public abstract class RuleTrigger : Freezable { public abstract T Accept(IRuleTriggerVisitor visitor); + + public bool DeepEquals(RuleTrigger action) + { + return this.WithDeepEqual(action).IgnoreProperty(x => x.IsFrozen).Compare(); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs index d19ddfef4..9fc87ea9d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs +++ b/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)); } + if (ids.SequenceEqual(fieldsOrdered.Select(x => x.Id))) + { + return this; + } + return Clone(clone => { clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToArray(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs index 1df458c5e..69f410fbf 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using DeepEqual.Syntax; namespace Squidex.Domain.Apps.Core.Schemas { @@ -44,5 +45,10 @@ namespace Squidex.Domain.Apps.Core.Schemas return new FieldNames(list); } + + public bool DeepEquals(FieldNames names) + { + return this.IsDeepEqual(names); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs index ae03e8cd4..17f7d2422 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.ObjectModel; +using DeepEqual.Syntax; 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 NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null); + + public bool DeepEquals(FieldProperties properties) + { + return this.WithDeepEqual(properties).IgnoreProperty(x => x.IsFrozen).Compare(); + } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs index 6fcef66be..5c0b0cfa0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs @@ -64,6 +64,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public NestedField Lock() { + if (isLocked) + { + return this; + } + return Clone(clone => { clone.isLocked = true; @@ -73,6 +78,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public NestedField Hide() { + if (isHidden) + { + return this; + } + return Clone(clone => { clone.isHidden = true; @@ -82,6 +92,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public NestedField Show() { + if (!isHidden) + { + return this; + } + return Clone(clone => { clone.isHidden = false; @@ -91,6 +106,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public NestedField Disable() { + if (isDisabled) + { + return this; + } + return Clone(clone => { clone.isDisabled = true; @@ -100,6 +120,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public NestedField Enable() { + if (!isDisabled) + { + return this; + } + return Clone(clone => { clone.isDisabled = false; diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs index 61ba3a6a8..63595e4c2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs +++ b/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); + typedProperties.Freeze(); + + if (properties.DeepEquals(typedProperties)) + { + return this; + } + return Clone>(clone => { clone.SetProperties(typedProperties); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs index af0f94d07..50cc4e6df 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs @@ -73,6 +73,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public RootField Lock() { + if (isLocked) + { + return this; + } + return Clone(clone => { clone.isLocked = true; @@ -82,6 +87,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public RootField Hide() { + if (isHidden) + { + return this; + } + return Clone(clone => { clone.isHidden = true; @@ -91,6 +101,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public RootField Show() { + if (!isHidden) + { + return this; + } + return Clone(clone => { clone.isHidden = false; @@ -100,6 +115,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public RootField Disable() { + if (isDisabled) + { + return this; + } + return Clone(clone => { clone.isDisabled = true; @@ -109,6 +129,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public RootField Enable() { + if (!isDisabled) + { + return this; + } + return Clone(clone => { clone.isDisabled = false; diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs index fffc1dc0b..ebff4b87c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs +++ b/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); + if (properties.DeepEquals(typedProperties)) + { + return this; + } + return Clone>(clone => { clone.SetProperties(typedProperties); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs index 4c8a4813f..eef72a2ac 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs @@ -116,21 +116,33 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public Schema Update(SchemaProperties newProperties) { - Guard.NotNull(newProperties); + newProperties ??= new SchemaProperties(); + + if (properties.DeepEquals(newProperties)) + { + return this; + } return Clone(clone => { clone.properties = newProperties; - clone.properties.Freeze(); + clone.Properties.Freeze(); }); } [Pure] public Schema ConfigureScripts(SchemaScripts newScripts) { + newScripts ??= new SchemaScripts(); + + if (scripts.DeepEquals(newScripts)) + { + return this; + } + return Clone(clone => { - clone.scripts = newScripts ?? new SchemaScripts(); + clone.scripts = newScripts; clone.scripts.Freeze(); }); } @@ -138,42 +150,55 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public Schema ConfigureFieldsInLists(FieldNames names) { + names ??= FieldNames.Empty; + + if (fieldsInLists.DeepEquals(names)) + { + return this; + } + return Clone(clone => { - clone.fieldsInLists = names ?? FieldNames.Empty; + clone.fieldsInLists = names; }); } [Pure] public Schema ConfigureFieldsInLists(params string[] names) { - return Clone(clone => - { - clone.fieldsInLists = new FieldNames(names); - }); + return ConfigureFieldsInLists(new FieldNames(names)); } [Pure] public Schema ConfigureFieldsInReferences(FieldNames names) { + names ??= FieldNames.Empty; + + if (fieldsInReferences.DeepEquals(names)) + { + return this; + } + return Clone(clone => { - clone.fieldsInReferences = names ?? FieldNames.Empty; + clone.fieldsInReferences = names; }); } [Pure] public Schema ConfigureFieldsInReferences(params string[] names) { - return Clone(clone => - { - clone.fieldsInReferences = new FieldNames(names); - }); + return ConfigureFieldsInReferences(new FieldNames(names)); } [Pure] public Schema Publish() { + if (isPublished) + { + return this; + } + return Clone(clone => { clone.isPublished = true; @@ -183,6 +208,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public Schema Unpublish() { + if (!isPublished) + { + return this; + } + return Clone(clone => { clone.isPublished = false; @@ -192,6 +222,11 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public Schema ChangeCategory(string newCategory) { + if (string.Equals(category, newCategory)) + { + return this; + } + return Clone(clone => { clone.category = newCategory; @@ -201,9 +236,16 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public Schema ConfigurePreviewUrls(IReadOnlyDictionary newPreviewUrls) { + previewUrls ??= EmptyPreviewUrls; + + if (previewUrls.EqualsDictionary(newPreviewUrls)) + { + return this; + } + return Clone(clone => { - clone.previewUrls = newPreviewUrls ?? EmptyPreviewUrls; + clone.previewUrls = newPreviewUrls; }); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs index be1851488..f9870f80b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs @@ -6,11 +6,17 @@ // ========================================================================== using System.Collections.ObjectModel; +using DeepEqual.Syntax; namespace Squidex.Domain.Apps.Core.Schemas { public sealed class SchemaProperties : NamedElementPropertiesBase { public ReadOnlyCollection Tags { get; set; } + + public bool DeepEquals(SchemaProperties properties) + { + return this.WithDeepEqual(properties).IgnoreProperty(x => x.IsFrozen).Compare(); + } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs index 6894f6f86..cda84a58c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using DeepEqual.Syntax; + namespace Squidex.Domain.Apps.Core.Schemas { public sealed class SchemaScripts : Freezable @@ -25,5 +27,10 @@ namespace Squidex.Domain.Apps.Core.Schemas public string Delete { get; set; } public string Query { get; set; } + + public bool DeepEquals(SchemaScripts scripts) + { + return this.WithDeepEqual(scripts).IgnoreProperty(x => x.IsFrozen).Compare(); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index d1a0c16f4..35e70070b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -10,6 +10,7 @@ True + diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs index 5d67798e5..68b6a6179 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs +++ b/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.Infrastructure; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; namespace Squidex.Domain.Apps.Core.EventSynchronization { public static class SchemaSynchronizer { - public static IEnumerable Synchronize(this Schema source, Schema? target, IJsonSerializer serializer, Func idGenerator, + public static IEnumerable Synchronize(this Schema source, Schema? target, Func idGenerator, SchemaSynchronizationOptions? options = null) { Guard.NotNull(source); - Guard.NotNull(serializer); Guard.NotNull(idGenerator); if (target == null) @@ -39,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization return @event; } - if (!source.Properties.EqualsJson(target.Properties, serializer)) + if (!source.Properties.DeepEquals(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 }); } - if (!source.Scripts.EqualsJson(target.Scripts, serializer)) + if (!source.Scripts.DeepEquals(target.Scripts)) { yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts }); } @@ -66,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization 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) { @@ -88,7 +86,6 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization private static IEnumerable SyncFields( FieldCollection source, FieldCollection target, - IJsonSerializer serializer, Func idGenerator, Func canUpdate, NamedId? parentId, SchemaSynchronizationOptions options) where T : class, IField @@ -131,7 +128,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization 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 }); } @@ -194,7 +191,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization { var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection.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) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs index 4670750b2..071ddd307 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using Squidex.Infrastructure.Json; namespace Squidex.Domain.Apps.Core.EventSynchronization { @@ -26,13 +25,5 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization { return lhs.GetType() == rhs.GetType(); } - - public static bool EqualsJson(this T lhs, T rhs, IJsonSerializer serializer) - { - var lhsJson = serializer.Serialize(lhs); - var rhsJson = serializer.Serialize(rhs); - - return string.Equals(lhsJson, rhsJson); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs index 0fda2ba53..4b1a75a5a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs +++ b/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 { + private static readonly EdmComplexType JsonType = new EdmComplexType("Squidex", "Json", null, false, true); private readonly EdmTypeFactory typeFactory; internal EdmTypeVisitor(EdmTypeFactory typeFactory) @@ -67,7 +68,7 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema public IEdmTypeReference? Visit(IField field) { - return null; + return new EdmComplexTypeReference(JsonType, !field.RawProperties.IsRequired); } public IEdmTypeReference? Visit(IField field) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs index 09e5aa07c..ec08af5ec 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs +++ b/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 }; } - 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) { - 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) { - 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) { - 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) { - 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) { - 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; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs index 35ad7fe26..e56831091 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs @@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema public JsonSchemaProperty? Visit(IField field) { - return Builder.StringProperty(); + return Builder.JsonProperty(); } public JsonSchemaProperty? Visit(IField field) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index 7e2ee9b19..505486f83 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text; using System.Text.RegularExpressions; using Squidex.Domain.Apps.Core.Contents; @@ -286,28 +287,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules 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)) - { - continue; - } - - if (value is JsonArray array && int.TryParse(path[j], out var idx) && idx >= 0 && idx < array.Count) - { - value = array[idx]; - } - else + if (!value.TryGetByPath(path.Skip(2), out value) || value == null || value.Type == JsonValueType.Null) { return Fallback; } } - if (value == null || value.Type == JsonValueType.Null) - { - return Fallback; - } - return value.ToString() ?? Fallback; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs index 9c8923bda..a91663107 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using Squidex.Domain.Apps.Core.Assets; namespace Squidex.Domain.Apps.Core.ValidateContent { @@ -15,16 +16,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent long FileSize { get; } - bool IsImage { get; } - - int? PixelWidth { get; } - - int? PixelHeight { get; } - string FileName { get; } string FileHash { get; } string Slug { get; } + + AssetMetadata Metadata { get; } + + AssetType Type { get; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs index a991b4e18..bc0103253 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; @@ -61,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators addError(path, "Invalid file extension."); } - if (!asset.IsImage) + if (asset.Type != AssetType.Image) { if (properties.MustBeImage) { @@ -71,11 +72,13 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators continue; } - if (asset.PixelWidth.HasValue && - asset.PixelHeight.HasValue) + var pixelWidth = asset.Metadata.GetPixelWidth(); + var pixelHeight = asset.Metadata.GetPixelHeight(); + + if (pixelWidth.HasValue && pixelHeight.HasValue) { - var w = asset.PixelWidth.Value; - var h = asset.PixelHeight.Value; + var w = pixelWidth.Value; + var h = pixelHeight.Value; var actualRatio = (double)w / h; diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index f0d9eaa82..e122953b5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -10,8 +10,10 @@ using System.Collections.Generic; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using NodaTime; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { @@ -67,21 +69,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets [BsonElement("fv")] public long FileVersion { get; set; } - [BsonRequired] - [BsonElement("im")] - public bool IsImage { get; set; } - [BsonRequired] [BsonElement("vs")] public long Version { get; set; } [BsonRequired] - [BsonElement("pw")] - public int? PixelWidth { get; set; } - - [BsonRequired] - [BsonElement("ph")] - public int? PixelHeight { get; set; } + [BsonElement("at")] + public AssetType Type { get; set; } [BsonRequired] [BsonElement("cb")] @@ -99,6 +93,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets [BsonElement("dl")] public bool IsDeleted { get; set; } + [BsonJson] + [BsonRequired] + [BsonElement("md")] + public AssetMetadata Metadata { get; set; } + public Guid AssetId { get { return Id; } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs index 26eca0f75..ce349a0fc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Linq; using MongoDB.Bson; using MongoDB.Driver; -using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.Queries; @@ -19,19 +18,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors public static class FindExtensions { private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - private static readonly SortDefinitionBuilder Sorting = Builders.Sort; public static ClrQuery AdjustToModel(this ClrQuery query) { if (query.Filter != null) { - query.Filter = PascalCasePathConverter.Transform(query.Filter); + query.Filter = FirstPascalPathConverter.Transform(query.Filter); } query.Sort = query.Sort .Select(x => new SortNode( - x.Path.Select(p => p.ToPascalCase()).ToList(), + x.Path.ToFirstPascalCase(), x.Order)) .ToList(); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathConverter.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathConverter.cs new file mode 100644 index 000000000..5c055e501 --- /dev/null +++ b/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 : TransformVisitor + { + private static readonly FirstPascalPathConverter Instance = new FirstPascalPathConverter(); + + private FirstPascalPathConverter() + { + } + + public static FilterNode? Transform(FilterNode node) + { + return node.Accept(Instance); + } + + public override FilterNode? Visit(CompareFilter nodeIn) + { + return new CompareFilter(nodeIn.Path.ToFirstPascalCase(), nodeIn.Operator, nodeIn.Value); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathExtension.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathExtension.cs new file mode 100644 index 000000000..84e670712 --- /dev/null +++ b/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); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoIndexOutput.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoIndexOutput.cs index 51c3fc88d..794124d6c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoIndexOutput.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoIndexOutput.cs @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.FullText try { - indexDirectory.Bucket.UploadFromStream(fullName, indexFileName, fs, options); + indexDirectory.Bucket.UploadFromStream(fullName, indexFileName, fs, options); } catch (MongoBulkWriteException ex) when (ex.WriteErrors.Any(x => x.Code == 11000)) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs index 02239fde6..5cad9f7c1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; 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."); } - - if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase)) - { - e("App has already this plan."); - } }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs index bc08332f9..9a70d1ebb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs +++ b/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."); } - if (contributors.TryGetValue(command.ContributorId, out var role)) - { - if (role == command.Role) - { - e(Not.New("Contributor", "role"), nameof(command.Role)); - } - } - else + if (!contributors.TryGetValue(command.ContributorId, out var role)) { if (plan != null && plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index fb600d5b7..d1300839f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Runtime.Serialization; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; @@ -56,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State [DataMember] public bool IsArchived { get; set; } - public void ApplyEvent(IEvent @event) + public override bool ApplyEvent(IEvent @event) { switch (@event) { @@ -64,174 +65,91 @@ namespace Squidex.Domain.Apps.Entities.Apps.State { 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); - break; + return true; } case AppImageUploaded e: - { - Image = e.Image; - - break; - } + return UpdateImage(e, ev => ev.Image); - case AppImageRemoved _: - { - Image = null; + case AppImageRemoved e when Image != null: + return UpdateImage(e, ev => null); - break; - } - - case AppPlanChanged e: - { - Plan = AppPlan.Build(e.Actor, e.PlanId); + case AppPlanChanged e when !string.Equals(Plan?.PlanId, e.PlanId): + return UpdatePlan(e, ev => AppPlan.Build(ev.Actor, ev.PlanId)); - break; - } - - case AppPlanReset _: - { - Plan = null; - - break; - } + case AppPlanReset e when Plan != null: + return UpdatePlan(e, ev => null); case AppContributorAssigned e: - { - Contributors = Contributors.Assign(e.ContributorId, e.Role); - - break; - } + return UpdateContributors(e, (ev, c) => c.Assign(ev.ContributorId, ev.Role)); case AppContributorRemoved e: - { - Contributors = Contributors.Remove(e.ContributorId); - - break; - } + return UpdateContributors(e, (ev, c) => c.Remove(ev.ContributorId)); case AppClientAttached e: - { - Clients = Clients.Add(e.Id, e.Secret); - - break; - } + return UpdateClients(e, (ev, c) => c.Add(ev.Id, ev.Secret)); case AppClientUpdated e: - { - Clients = Clients.Update(e.Id, e.Role); - - break; - } + return UpdateClients(e, (ev, c) => c.Update(ev.Id, ev.Role)); case AppClientRenamed e: - { - Clients = Clients.Rename(e.Id, e.Name); - - break; - } + return UpdateClients(e, (ev, c) => c.Rename(ev.Id, ev.Name)); case AppClientRevoked e: - { - Clients = Clients.Revoke(e.Id); - - break; - } + return UpdateClients(e, (ev, c) => c.Revoke(ev.Id)); case AppWorkflowAdded e: - { - Workflows = Workflows.Add(e.WorkflowId, e.Name); - - break; - } + return UpdateWorkflows(e, (ev, w) => w.Add(ev.WorkflowId, ev.Name)); case AppWorkflowUpdated e: - { - Workflows = Workflows.Update(e.WorkflowId, e.Workflow); - - break; - } + return UpdateWorkflows(e, (ev, w) => w.Update(ev.WorkflowId, ev.Workflow)); case AppWorkflowDeleted e: - { - Workflows = Workflows.Remove(e.WorkflowId); - - break; - } + return UpdateWorkflows(e, (ev, w) => w.Remove(ev.WorkflowId)); case AppPatternAdded e: - { - Patterns = Patterns.Add(e.PatternId, e.Name, e.Pattern, e.Message); - - break; - } + return UpdatePatterns(e, (ev, p) => p.Add(ev.PatternId, ev.Name, ev.Pattern, ev.Message)); case AppPatternDeleted e: - { - Patterns = Patterns.Remove(e.PatternId); - - break; - } + return UpdatePatterns(e, (ev, p) => p.Remove(ev.PatternId)); case AppPatternUpdated e: - { - Patterns = Patterns.Update(e.PatternId, e.Name, e.Pattern, e.Message); - - break; - } + return UpdatePatterns(e, (ev, p) => p.Update(ev.PatternId, ev.Name, ev.Pattern, ev.Message)); case AppRoleAdded e: - { - Roles = Roles.Add(e.Name); - - break; - } - - case AppRoleDeleted e: - { - Roles = Roles.Remove(e.Name); - - break; - } + return UpdateRoles(e, (ev, r) => r.Add(ev.Name)); case AppRoleUpdated e: - { - Roles = Roles.Update(e.Name, e.Permissions); + return UpdateRoles(e, (ev, r) => r.Update(ev.Name, ev.Permissions)); - break; - } + case AppRoleDeleted e: + return UpdateRoles(e, (ev, r) => r.Remove(ev.Name)); case AppLanguageAdded e: - { - LanguagesConfig = LanguagesConfig.Set(e.Language); - - break; - } + return UpdateLanguages(e, (ev, l) => l.Set(ev.Language)); case AppLanguageRemoved e: - { - LanguagesConfig = LanguagesConfig.Remove(e.Language); - - break; - } + return UpdateLanguages(e, (ev, l) => l.Remove(ev.Language)); case AppLanguageUpdated e: + return UpdateLanguages(e, (ev, l) => { - LanguagesConfig = LanguagesConfig.Set(e.Language, e.IsOptional, e.Fallback); + l = l.Set(ev.Language, ev.IsOptional, ev.Fallback); - if (e.IsMaster) + if (ev.IsMaster) { - LanguagesConfig = LanguagesConfig.MakeMaster(e.Language); + LanguagesConfig = LanguagesConfig.MakeMaster(ev.Language); } - break; - } + return l; + }); case AppArchived _: { @@ -239,14 +157,79 @@ namespace Squidex.Domain.Apps.Entities.Apps.State IsArchived = true; - break; + return true; } } + + return false; + } + + private bool UpdateContributors(T @event, Func update) + { + var previous = Contributors; + + Contributors = update(@event, previous); + + return !ReferenceEquals(previous, Contributors); + } + + private bool UpdateClients(T @event, Func update) + { + var previous = Clients; + + Clients = update(@event, previous); + + return !ReferenceEquals(previous, Clients); } - public override AppState Apply(Envelope @event) + private bool UpdateLanguages(T @event, Func update) { - return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); + var previous = LanguagesConfig; + + LanguagesConfig = update(@event, previous); + + return !ReferenceEquals(previous, LanguagesConfig); + } + + private bool UpdatePatterns(T @event, Func update) + { + var previous = Patterns; + + Patterns = update(@event, previous); + + return !ReferenceEquals(previous, Patterns); + } + + private bool UpdateRoles(T @event, Func update) + { + var previous = Roles; + + Roles = update(@event, previous); + + return !ReferenceEquals(previous, Roles); + } + + private bool UpdateWorkflows(T @event, Func update) + { + var previous = Workflows; + + Workflows = update(@event, previous); + + return !ReferenceEquals(previous, Workflows); + } + + private bool UpdateImage(T @event, Func update) + { + Image = update(@event); + + return true; + } + + private bool UpdatePlan(T @event, Func update) + { + Plan = update(@event); + + return true; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index 647bd16bf..c9c446945 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -11,7 +11,6 @@ using System.Security.Cryptography; using System.Threading.Tasks; using Orleans; using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; @@ -23,33 +22,29 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly IAssetFileStore assetFileStore; private readonly IAssetEnricher assetEnricher; private readonly IAssetQueryService assetQuery; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IContextProvider contextProvider; - private readonly IEnumerable> tagGenerators; + private readonly IEnumerable assetMetadataSources; public AssetCommandMiddleware( IGrainFactory grainFactory, IAssetEnricher assetEnricher, - IAssetQueryService assetQuery, IAssetFileStore assetFileStore, - IAssetThumbnailGenerator assetThumbnailGenerator, + IAssetQueryService assetQuery, IContextProvider contextProvider, - IEnumerable> tagGenerators) + IEnumerable assetMetadataSources) : base(grainFactory) { Guard.NotNull(assetEnricher); Guard.NotNull(assetFileStore); Guard.NotNull(assetQuery); - Guard.NotNull(assetThumbnailGenerator); + Guard.NotNull(assetMetadataSources); Guard.NotNull(contextProvider); - Guard.NotNull(tagGenerators); this.assetFileStore = assetFileStore; this.assetEnricher = assetEnricher; this.assetQuery = assetQuery; - this.assetThumbnailGenerator = assetThumbnailGenerator; this.contextProvider = contextProvider; - this.tagGenerators = tagGenerators; + this.assetMetadataSources = assetMetadataSources; } public override async Task HandleAsync(CommandContext context, Func next) @@ -60,7 +55,6 @@ namespace Squidex.Domain.Apps.Entities.Assets { case CreateAsset createAsset: { - await EnrichWithImageInfosAsync(createAsset); await EnrichWithHashAndUploadAsync(createAsset, tempFile); try @@ -82,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - GenerateTags(createAsset); + await EnrichWithMetadataAsync(createAsset, createAsset.Tags); await HandleCoreAsync(context, next); @@ -102,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Assets case UpdateAsset updateAsset: { - await EnrichWithImageInfosAsync(updateAsset); + await EnrichWithMetadataAsync(updateAsset); await EnrichWithHashAndUploadAsync(updateAsset, tempFile); try @@ -144,11 +138,6 @@ namespace Squidex.Domain.Apps.Entities.Assets 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) { 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? tags = null) { - if (createAsset.Tags == null) - { - createAsset.Tags = new HashSet(); - } - - foreach (var tagGenerator in tagGenerators) + foreach (var metadataSource in assetMetadataSources) { - tagGenerator.GenerateTags(createAsset, createAsset.Tags); + await metadataSource.EnhanceAsync(command, tags); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs index 9424d8c57..77b33cd19 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using NodaTime; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Assets @@ -42,17 +43,17 @@ namespace Squidex.Domain.Apps.Entities.Assets public string Slug { get; set; } + public string MetadataText { get; set; } + public long FileSize { get; set; } public long FileVersion { get; set; } - public bool IsImage { 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 { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs index 49525def9..bed6848d5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Assets case RenameAssetFolder renameAssetFolder: return UpdateReturn(renameAssetFolder, c => { - GuardAssetFolder.CanRename(c, Snapshot.FolderName); + GuardAssetFolder.CanRename(c); Rename(c); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index 6a625423e..2c27323a7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Assets case AnnotateAsset annotateAsset: return UpdateReturnAsync(annotateAsset, async c => { - GuardAsset.CanAnnotate(c, Snapshot.FileName!, Snapshot.Slug); + GuardAsset.CanAnnotate(c); 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 { - IsImage = command.ImageInfo != null, FileName = command.File.FileName, FileSize = command.File.FileSize, FileVersion = 0, MimeType = command.File.MimeType, - PixelWidth = command.ImageInfo?.PixelWidth, - PixelHeight = command.ImageInfo?.PixelHeight, Slug = command.File.FileName.ToAssetSlug() }); @@ -147,10 +144,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { FileVersion = Snapshot.FileVersion + 1, FileSize = command.File.FileSize, - MimeType = command.File.MimeType, - PixelWidth = command.ImageInfo?.PixelWidth, - PixelHeight = command.ImageInfo?.PixelHeight, - IsImage = command.ImageInfo != null + MimeType = command.File.MimeType }); RaiseEvent(@event); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs index 5ef8c77e0..18e52c5a6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Assets; namespace Squidex.Domain.Apps.Entities.Assets.Commands { @@ -16,5 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands public string? Slug { get; set; } public HashSet Tags { get; set; } + + public AssetMetadata Metadata { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs index 2dcab921d..5257a8e7a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs +++ b/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 HashSet Tags { get; set; } + public HashSet Tags { get; } = new HashSet(); public CreateAsset() { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs index 6a519931c..c0f6d847f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure.Assets; namespace Squidex.Domain.Apps.Entities.Assets.Commands @@ -13,7 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands { 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; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs new file mode 100644 index 000000000..dc2aaf79d --- /dev/null +++ b/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? tags) + { + Enhance(command, tags); + + return TaskHelper.Done; + } + + private void Enhance(UploadAssetCommand command, HashSet? 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 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; + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs index 88df12a99..fc1cbe6e1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs @@ -6,22 +6,33 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class FileTypeTagGenerator : ITagGenerator + public sealed class FileTypeTagGenerator : IAssetMetadataSource { - public void GenerateTags(CreateAsset source, HashSet tags) + public Task EnhanceAsync(UploadAssetCommand command, HashSet? tags) { - var extension = source.File?.FileName?.FileType(); - - if (!string.IsNullOrWhiteSpace(extension)) + if (tags != null) { - tags.Add($"type/{extension.ToLowerInvariant()}"); + var extension = command.File?.FileName?.FileType(); + + if (!string.IsNullOrWhiteSpace(extension)) + { + tags.Add($"type/{extension.ToLowerInvariant()}"); + } } + + return TaskHelper.Done; + } + + public IEnumerable Format(IAssetEntity asset) + { + yield break; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs index edc0ba2c5..99e356337 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs +++ b/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 void CanAnnotate(AnnotateAsset command, string oldFileName, string oldSlug) + public static void CanAnnotate(AnnotateAsset command) { Guard.NotNull(command); @@ -23,9 +23,14 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards { if (string.IsNullOrWhiteSpace(command.FileName) && 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 => { - if (command.ParentId == oldParentId) - { - e("Asset is already part of this folder.", nameof(command.ParentId)); - } - else + if (command.ParentId != oldParentId) { await CheckPathAsync(command.ParentId, assetQuery, e); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs index 111eb1b85..4e2b270cd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs +++ b/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); @@ -40,10 +40,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards { 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 => { - if (command.ParentId == oldParentId) - { - e("Asset folder is already part of this folder.", nameof(command.ParentId)); - } - else + if (command.ParentId != oldParentId) { await CheckPathAsync(command.ParentId, assetQuery, id, e); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs new file mode 100644 index 000000000..8b4c64cf5 --- /dev/null +++ b/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? tags); + + IEnumerable Format(IAssetEntity asset); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs index eab0cde16..8f1daa520 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs @@ -12,5 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public interface IEnrichedAssetEntity : IAssetEntity { HashSet TagNames { get; } + + string MetadataText { get; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageMetadataSource.cs new file mode 100644 index 000000000..cf929ddc6 --- /dev/null +++ b/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? 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 Format(IAssetEntity asset) + { + if (asset.Type == AssetType.Image) + { + yield return $"{asset.Metadata.GetPixelWidth()}x{asset.Metadata.GetPixelHeight()}px"; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs deleted file mode 100644 index c44b7073a..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs +++ /dev/null @@ -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 - { - public void GenerateTags(CreateAsset source, HashSet 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"); - } - } - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs index 78cdafee7..ab7ad8d6f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Tags; using Squidex.Infrastructure; @@ -18,12 +19,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries public sealed class AssetEnricher : IAssetEnricher { private readonly ITagService tagService; + private readonly IEnumerable assetMetadataSources; - public AssetEnricher(ITagService tagService) + public AssetEnricher(ITagService tagService, IEnumerable assetMetadataSources) { Guard.NotNull(tagService); + Guard.NotNull(assetMetadataSources); this.tagService = tagService; + this.assetMetadataSources = assetMetadataSources; } public async Task EnrichAsync(IAssetEntity asset, Context context) @@ -48,12 +52,49 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries if (ShouldEnrich(context)) { await EnrichTagsAsync(results); + + EnrichWithMetadataText(results); } return results; } } + private void EnrichWithMetadataText(List 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 assets) { foreach (var group in assets.GroupBy(x => x.AppId.Id)) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs index f91b8dd8d..193670562 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs +++ b/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.Created), JsonObjectType.String, JsonFormatStrings.DateTime); 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.FileName), JsonObjectType.String); AddProperty(nameof(IAssetEntity.FileSize), 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.PixelHeight), JsonObjectType.Integer); - AddProperty(nameof(IAssetEntity.PixelWidth), JsonObjectType.Integer); AddProperty(nameof(IAssetEntity.Slug), JsonObjectType.String); AddProperty(nameof(IAssetEntity.Tags), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.Type), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.Version), JsonObjectType.Integer); return schema; } @@ -142,22 +141,29 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries 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.Created), EdmPrimitiveTypeKind.DateTimeOffset); 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.FileName), EdmPrimitiveTypeKind.String); AddProperty(nameof(IAssetEntity.FileSize), 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.PixelHeight), EdmPrimitiveTypeKind.Int32); - AddProperty(nameof(IAssetEntity.PixelWidth), EdmPrimitiveTypeKind.Int32); AddProperty(nameof(IAssetEntity.Slug), 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"); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs index 6d6ce90cc..ff3de8bc6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs +++ b/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 { - public class AssetFolderState : DomainObjectState, IAssetFolderEntity + public sealed class AssetFolderState : DomainObjectState, IAssetFolderEntity { [DataMember] public NamedId AppId { get; set; } @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.State [DataMember] public Guid ParentId { get; set; } - public void ApplyEvent(IEvent @event) + public override bool ApplyEvent(IEvent @event) { switch (@event) { @@ -38,35 +38,32 @@ namespace Squidex.Domain.Apps.Entities.Assets.State { 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; - break; + return true; } case AssetFolderDeleted _: { IsDeleted = true; - break; + return true; } } - } - public override AssetFolderState Apply(Envelope @event) - { - return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); + return false; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs index e271c75fc..8a39a1a60 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Runtime.Serialization; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; @@ -47,26 +48,23 @@ namespace Squidex.Domain.Apps.Entities.Assets.State public long TotalSize { get; set; } [DataMember] - public bool IsImage { get; set; } + public HashSet Tags { get; set; } [DataMember] - public int? PixelWidth { get; set; } + public AssetMetadata Metadata { get; set; } [DataMember] - public int? PixelHeight { get; set; } + public AssetType Type { get; set; } [DataMember] public bool IsDeleted { get; set; } - [DataMember] - public HashSet Tags { get; set; } - public Guid AssetId { get { return Id; } } - public void ApplyEvent(IEvent @event) + public override bool ApplyEvent(IEvent @event) { switch (@event) { @@ -87,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.State TotalSize += e.FileSize; - break; + return true; } case AssetUpdated e: @@ -96,48 +94,60 @@ namespace Squidex.Domain.Apps.Entities.Assets.State TotalSize += e.FileSize; - break; + return true; } case AssetAnnotated e: { - if (!string.IsNullOrWhiteSpace(e.FileName)) + var hasChanged = false; + + if (!string.IsNullOrWhiteSpace(e.FileName) && !string.Equals(e.FileName, FileName)) { FileName = e.FileName; + + hasChanged = true; } - if (!string.IsNullOrWhiteSpace(e.Slug)) + if (!string.IsNullOrWhiteSpace(e.Slug) && !string.Equals(e.Slug, Slug)) { Slug = e.Slug; + + hasChanged = true; } - if (e.Tags != null) + if (e.Tags != null && !e.Tags.SetEquals(Tags)) { Tags = e.Tags; + + hasChanged = true; } - break; + if (e.Metadata != null && !e.Metadata.EqualsDictionary(Metadata)) + { + Metadata = e.Metadata; + + hasChanged = true; + } + + return hasChanged; } - case AssetMoved e: + case AssetMoved e when e.ParentId != ParentId: { ParentId = e.ParentId; - break; + return true; } case AssetDeleted _: { IsDeleted = true; - break; + return true; } } - } - public override AssetState Apply(Envelope @event) - { - return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); + return false; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs index 4c4a4bf80..51ecdf4f4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL 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)); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs index eabe2f8b8..01e8ee4fc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs +++ b/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); - (IGraphType? ResolveType, ValueResolver? Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName); + (IGraphType?, ValueResolver?, QueryArguments?) GetGraphType(ISchemaEntity schema, IField field, string fieldName); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs index c8ee5d2f2..0cb3178d5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs @@ -7,12 +7,15 @@ using System; using GraphQL.Types; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public static class AllTypes { + public const string PathName = "path"; + public static readonly Type None = typeof(NoopGraphType); public static readonly Type NonNullTagsType = typeof(NonNullGraphType>>); @@ -31,6 +34,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static readonly IGraphType Boolean = new BooleanGraphType(); + public static readonly IGraphType AssetType = new EnumerationGraphType(); + public static readonly IGraphType NonNullInt = new NonNullGraphType(Int); 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 NonNullAssetType = new NonNullGraphType(AssetType); + public static readonly IGraphType NoopDate = new NoopGraphType(Date); 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 NoopTags = new NoopGraphType("Tags"); + public static readonly IGraphType NoopTags = new NoopGraphType("TagsScalar"); + + public static readonly IGraphType NoopGeolocation = new NoopGraphType("GeolocationScalar"); - public static readonly IGraphType NoopGeolocation = new NoopGraphType("Geolocation"); + public static readonly QueryArguments PathArguments = new QueryArguments(new QueryArgument(None) + { + Name = PathName, + Description = $"The path to the json value", + DefaultValue = null, + ResolvedType = String + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs index 128fd9eb5..b079e203e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs +++ b/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 { Name = $"find{schemaType}Content", - Arguments = CreateContentFindTypes(schemaName), + Arguments = CreateContentFindArguments(schemaName), ResolvedType = contentType, 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 { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs index 0ef994ec0..29124bf92 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs @@ -8,6 +8,7 @@ using System; using GraphQL.Resolvers; using GraphQL.Types; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; @@ -143,24 +144,43 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "isImage", ResolvedType = AllTypes.NonNullBoolean, - Resolver = Resolve(x => x.IsImage), - Description = "Determines of the created file is an image." + Resolver = Resolve(x => x.Type == AssetType.Image), + Description = "Determines if the uploaded file is an image.", + DeprecationReason = "Use 'type' field instead." }); AddField(new FieldType { Name = "pixelWidth", ResolvedType = AllTypes.Int, - Resolver = Resolve(x => x.PixelWidth), - Description = "The width of the image in pixels if the asset is an image." + Resolver = Resolve(x => x.Metadata.GetPixelWidth()), + Description = "The width of the image in pixels if the asset is an image.", + DeprecationReason = "Use 'metadata' field instead." }); AddField(new FieldType { Name = "pixelHeight", ResolvedType = AllTypes.Int, - Resolver = Resolve(x => x.PixelHeight), - Description = "The height of the image in pixels if the asset is an image." + Resolver = Resolve(x => x.Metadata.GetPixelHeight()), + 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 @@ -172,6 +192,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Type = AllTypes.NonNullTagsType }); + AddField(new FieldType + { + Name = "metadata", + Arguments = AllTypes.PathArguments, + ResolvedType = AllTypes.NoopJson, + Resolver = ResolveMetadata(), + Description = "The asset metadata.", + }); + if (model.CanGenerateAssetSourceUrl) { AddField(new FieldType @@ -186,6 +215,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = "An asset"; } + private static IFieldResolver ResolveMetadata() + { + return new FuncFieldResolver(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 action) { return new FuncFieldResolver(c => action(c.Source)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs index 455387497..14314ca5e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs +++ b/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()) { - var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); + var (resolvedType, valueResolver, args) = model.GetGraphType(schema, field, fieldName); if (valueResolver != null) { AddField(new FieldType { Name = fieldName, + Arguments = args, Resolver = PartitionResolver(valueResolver, field.Name), ResolvedType = resolvedType, Description = field.RawProperties.Hints diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs index e675c8cd8..35e63f455 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/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()) { - var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); + var (resolvedType, valueResolver, args) = model.GetGraphType(schema, field, fieldName); if (valueResolver != null) { @@ -44,6 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types fieldGraphType.AddField(new FieldType { Name = key.EscapePartition(), + Arguments = args, Resolver = PartitionResolver(valueResolver, key), ResolvedType = resolvedType, Description = field.RawProperties.Hints diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs index 3a450d944..b94045f27 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs +++ b/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()) { - var (resolveType, valueResolver) = model.GetGraphType(schema, nestedField, nestedName); + var (resolveType, valueResolver, args) = model.GetGraphType(schema, nestedField, nestedName); if (resolveType != null && valueResolver != null) { @@ -35,6 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = nestedName, + Arguments = args, Resolver = resolver, ResolvedType = resolveType, Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs index 5867a7f07..3efa4bdaa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs +++ b/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 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 readonly Dictionary schemaTypes; @@ -40,74 +40,83 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types this.fieldName = fieldName; } - public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IArrayField field) + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IArrayField field) { return ResolveNested(field); } - public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField field) { return ResolveAssets(); } - public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField field) { return ResolveDefault(AllTypes.NoopBoolean); } - public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField field) { return ResolveDefault(AllTypes.NoopDate); } - public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField field) { return ResolveDefault(AllTypes.NoopGeolocation); } - public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopJson); - } - - public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField field) { return ResolveDefault(AllTypes.NoopFloat); } - public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField field) { return ResolveReferences(field); } - public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField field) { return ResolveDefault(AllTypes.NoopString); } - public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField field) { return ResolveDefault(AllTypes.NoopTags); } - public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField 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))); - return (schemaFieldType, NoopResolver); + return (schemaFieldType, NoopResolver, null); + } + + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField 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) => { @@ -116,10 +125,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return context.GetReferencedAssetsAsync(value); }); - return (assetListType, resolver); + return (assetListType, resolver, null); } - private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveReferences(IField field) + private (IGraphType?, ValueResolver?, QueryArguments?) ResolveReferences(IField field) { IGraphType contentType = schemaTypes.GetOrDefault(field.Properties.SingleId()); @@ -129,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types if (!union.PossibleTypes.Any()) { - return (null, null); + return (null, null, null); } contentType = union; @@ -144,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType)); - return (schemaFieldType, resolver); + return (schemaFieldType, resolver, null); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs index 6540fd92a..5788d9412 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs +++ b/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() { - Name = "Json"; + Name = "JsonScalar"; Description = "Unstructured Json object"; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 0878e695c..9d18248fc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ExtractReferenceIds; @@ -249,7 +250,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries field.GetReferencedIds(partitionValue, Ids.ContentOnly) .Select(x => assets[x]) .SelectMany(x => x) - .FirstOrDefault(x => x.IsImage); + .FirstOrDefault(x => x.Type == AssetType.Image); if (referencedImage != null) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index 8e13695c5..bb9b20557 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/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 { - public class ContentState : DomainObjectState, IContentEntity + public sealed class ContentState : DomainObjectState, IContentEntity { [DataMember] public NamedId AppId { get; set; } @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.State [DataMember] public Status Status { get; set; } - public void ApplyEvent(IEvent @event) + public override bool ApplyEvent(IEvent @event) { switch (@event) { @@ -121,11 +121,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.State break; } } - } - public override ContentState Apply(Envelope @event) - { - return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); + return true; } private void UpdateData(NamedContentData? data, NamedContentData? dataDraft, bool isPending) diff --git a/backend/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/DomainEntityExtensions.cs similarity index 93% rename from backend/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/DomainEntityExtensions.cs index b5c78cd43..95019e1a0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/DomainEntityExtensions.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities { - public static class EntityExtensions + public static class DomainEntityExtensions { public static NamedId NamedId(this IAppEntity entity) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index 7bafa9c44..88643a701 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -8,22 +8,20 @@ using System; using System.Runtime.Serialization; using NodaTime; +using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities { - public abstract class DomainObjectState : Cloneable, + public abstract class DomainObjectState : IDomainState, IEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, - IEntityWithVersion, - IUpdateableEntity, - IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy - where T : Cloneable + IEntityWithVersion + where T : class { [DataMember] public Guid Id { get; set; } @@ -43,11 +41,38 @@ namespace Squidex.Domain.Apps.Entities [DataMember] public long Version { get; set; } = EtagVersion.Empty; - public abstract T Apply(Envelope @event); + public abstract bool ApplyEvent(IEvent @event); - public T Clone() + public T Apply(Envelope @event) { - return Clone(x => { }); + var payload = (SquidexEvent)@event.Payload; + + var clone = (DomainObjectState)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)!; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs deleted file mode 100644 index 9e13f4f36..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs +++ /dev/null @@ -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(this T entity, Envelope envelope, Action? 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; - } - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs deleted file mode 100644 index b29780c52..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs +++ /dev/null @@ -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; } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs deleted file mode 100644 index 0f7206e21..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs +++ /dev/null @@ -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; } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs deleted file mode 100644 index 50b8947d1..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs +++ /dev/null @@ -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; } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs index 40023a767..4717de200 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs @@ -7,7 +7,6 @@ using System; using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Infrastructure; 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); @@ -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); - - 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); - - if (!rule.IsEnabled) - { - throw new DomainException("Rule is already disabled."); - } } public static void CanDelete(DeleteRule command) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs index 1722572fd..6c286ac0c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Rules case UpdateRule updateRule: 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); @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Rules case EnableRule enableRule: return UpdateReturn(enableRule, c => { - GuardRule.CanEnable(c, Snapshot.RuleDef); + GuardRule.CanEnable(c); Enable(c); @@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Rules case DisableRule disableRule: return UpdateReturn(disableRule, c => { - GuardRule.CanDisable(c, Snapshot.RuleDef); + GuardRule.CanDisable(c); Disable(c); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs index bf45aead8..e43745d43 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs +++ b/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 { [CollectionName("Rules")] - public class RuleState : DomainObjectState, IRuleEntity + public sealed class RuleState : DomainObjectState, IRuleEntity { [DataMember] public NamedId AppId { get; set; } @@ -29,8 +29,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.State [DataMember] public bool IsDeleted { get; set; } - public void ApplyEvent(IEvent @event) + public override bool ApplyEvent(IEvent @event) { + var previousRule = RuleDef; + switch (@event) { case RuleCreated e: @@ -40,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.State AppId = e.AppId; - break; + return true; } case RuleUpdated e: @@ -81,14 +83,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.State { IsDeleted = true; - break; + return true; } } - } - public override RuleState Apply(Envelope @event) - { - return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); + return !ReferenceEquals(previousRule, RuleDef); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs index cdf35c8b5..31ce88343 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs +++ b/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); @@ -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); - 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); - - 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); - - 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); } - public static void CanConfigureScripts(Schema schema, ConfigureScripts command) + public static void CanConfigureScripts(ConfigureScripts command) { Guard.NotNull(command); } - public static void CanChangeCategory(Schema schema, ChangeCategory command) + public static void CanChangeCategory(ChangeCategory command) { Guard.NotNull(command); } - public static void CanDelete(Schema schema, DeleteSchema command) + public static void CanDelete(DeleteSchema command) { Guard.NotNull(command); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs index 424a8e3f2..bdc71884a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs +++ b/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 void CanAdd(Schema schema, AddField command) + public static void CanAdd(AddField command, Schema schema) { 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); @@ -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); var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - if (field.IsHidden) - { - throw new DomainException("Schema field is already hidden."); - } - - if (!field.IsForApi()) + if (!field.IsForApi(true)) { 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); 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); 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)) { - 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); 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); - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (!field.IsDisabled) - { - throw new DomainException("Schema field is already enabled."); - } + GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); } - public static void CanLock(Schema schema, LockField command) + public static void CanLock(LockField command, Schema schema) { Guard.NotNull(command); - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (field.IsLocked) - { - throw new DomainException("Schema field is already locked."); - } + GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, true); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs index a06717cdb..cc7793d98 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ b/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.Commands; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; @@ -27,14 +26,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas { public sealed class SchemaGrain : DomainObjectGrain, ISchemaGrain { - private readonly IJsonSerializer serializer; - - public SchemaGrain(IStore store, ISemanticLog log, IJsonSerializer serializer) + public SchemaGrain(IStore store, ISemanticLog log) : base(store, log) { - Guard.NotNull(serializer); - - this.serializer = serializer; } protected override Task ExecuteAsync(IAggregateCommand command) @@ -46,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case AddField addField: return UpdateReturn(addField, c => { - GuardSchemaField.CanAdd(Snapshot.SchemaDef, c); + GuardSchemaField.CanAdd(c, Snapshot.SchemaDef); Add(c); @@ -87,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case DeleteField deleteField: return UpdateReturn(deleteField, c => { - GuardSchemaField.CanDelete(Snapshot.SchemaDef, deleteField); + GuardSchemaField.CanDelete(deleteField, Snapshot.SchemaDef); DeleteField(c); @@ -97,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case LockField lockField: return UpdateReturn(lockField, c => { - GuardSchemaField.CanLock(Snapshot.SchemaDef, lockField); + GuardSchemaField.CanLock(lockField, Snapshot.SchemaDef); LockField(c); @@ -107,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case HideField hideField: return UpdateReturn(hideField, c => { - GuardSchemaField.CanHide(Snapshot.SchemaDef, c); + GuardSchemaField.CanHide(c, Snapshot.SchemaDef); HideField(c); @@ -117,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case ShowField showField: return UpdateReturn(showField, c => { - GuardSchemaField.CanShow(Snapshot.SchemaDef, c); + GuardSchemaField.CanShow(c, Snapshot.SchemaDef); ShowField(c); @@ -127,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case DisableField disableField: return UpdateReturn(disableField, c => { - GuardSchemaField.CanDisable(Snapshot.SchemaDef, c); + GuardSchemaField.CanDisable(c, Snapshot.SchemaDef); DisableField(c); @@ -137,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case EnableField enableField: return UpdateReturn(enableField, c => { - GuardSchemaField.CanEnable(Snapshot.SchemaDef, c); + GuardSchemaField.CanEnable(c, Snapshot.SchemaDef); EnableField(c); @@ -147,7 +141,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case UpdateField updateField: return UpdateReturn(updateField, c => { - GuardSchemaField.CanUpdate(Snapshot.SchemaDef, c); + GuardSchemaField.CanUpdate(c, Snapshot.SchemaDef); UpdateField(c); @@ -157,7 +151,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case ReorderFields reorderFields: return UpdateReturn(reorderFields, c => { - GuardSchema.CanReorder(Snapshot.SchemaDef, c); + GuardSchema.CanReorder(c, Snapshot.SchemaDef); Reorder(c); @@ -167,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case UpdateSchema updateSchema: return UpdateReturn(updateSchema, c => { - GuardSchema.CanUpdate(Snapshot.SchemaDef, c); + GuardSchema.CanUpdate(c); Update(c); @@ -177,7 +171,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case PublishSchema publishSchema: return UpdateReturn(publishSchema, c => { - GuardSchema.CanPublish(Snapshot.SchemaDef, c); + GuardSchema.CanPublish(c); Publish(c); @@ -187,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case UnpublishSchema unpublishSchema: return UpdateReturn(unpublishSchema, c => { - GuardSchema.CanUnpublish(Snapshot.SchemaDef, c); + GuardSchema.CanUnpublish(c); Unpublish(c); @@ -197,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case ConfigureScripts configureScripts: return UpdateReturn(configureScripts, c => { - GuardSchema.CanConfigureScripts(Snapshot.SchemaDef, c); + GuardSchema.CanConfigureScripts(c); ConfigureScripts(c); @@ -207,7 +201,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case ChangeCategory changeCategory: return UpdateReturn(changeCategory, c => { - GuardSchema.CanChangeCategory(Snapshot.SchemaDef, c); + GuardSchema.CanChangeCategory(c); ChangeCategory(c); @@ -227,7 +221,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case ConfigureUIFields configureUIFields: return UpdateReturn(configureUIFields, c => { - GuardSchema.CanConfigureUIFields(Snapshot.SchemaDef, c); + GuardSchema.CanConfigureUIFields(c, Snapshot.SchemaDef); ConfigureUIFields(c); @@ -237,7 +231,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas case DeleteSchema deleteSchema: return Update(deleteSchema, c => { - GuardSchema.CanDelete(Snapshot.SchemaDef, c); + GuardSchema.CanDelete(c); Delete(c); }); @@ -258,7 +252,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var schemaSource = Snapshot.SchemaDef; 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) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index 31d3e58c6..7166a236b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/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 { [CollectionName("Schemas")] - public class SchemaState : DomainObjectState, ISchemaEntity + public sealed class SchemaState : DomainObjectState, ISchemaEntity { [DataMember] public NamedId AppId { get; set; } @@ -33,8 +33,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State [DataMember] public Schema SchemaDef { get; set; } - public void ApplyEvent(IEvent @event) + public override bool ApplyEvent(IEvent @event) { + var previousSchema = SchemaDef; + switch (@event) { case SchemaCreated e: @@ -44,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State AppId = e.AppId; - break; + return true; } case FieldAdded e: @@ -187,14 +189,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State { IsDeleted = true; - break; + return true; } } - } - public override SchemaState Apply(Envelope @event) - { - return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); + return !ReferenceEquals(previousSchema, SchemaDef); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index ea0d0ab46..97edb9cf8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -31,6 +31,7 @@ + ..\..\Squidex.ruleset diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs index b5ecfcb00..0fda9b944 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Assets @@ -17,6 +18,8 @@ namespace Squidex.Domain.Apps.Events.Assets public string Slug { get; set; } + public AssetMetadata? Metadata { get; set; } + public HashSet? Tags { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs index 7698c12b5..dc646ac0d 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs @@ -7,11 +7,12 @@ using System; using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Assets { - [EventType(nameof(AssetCreated))] + [EventType(nameof(AssetCreated), 2)] public sealed class AssetCreated : AssetEvent { public Guid ParentId { get; set; } @@ -28,11 +29,9 @@ namespace Squidex.Domain.Apps.Events.Assets public long FileSize { get; set; } - public bool IsImage { get; set; } + public AssetType Type { get; set; } - public int? PixelWidth { get; set; } - - public int? PixelHeight { get; set; } + public AssetMetadata Metadata { get; set; } public HashSet? Tags { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs index 0078025fb..1c162b1a6 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs @@ -5,11 +5,12 @@ // 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 { - [TypeName("AssetUpdated")] + [EventType(nameof(AssetUpdated), 2)] public sealed class AssetUpdated : AssetEvent { public string MimeType { get; set; } @@ -20,10 +21,8 @@ namespace Squidex.Domain.Apps.Events.Assets public long FileVersion { get; set; } - public bool IsImage { get; set; } + public AssetType Type { get; set; } - public int? PixelWidth { get; set; } - - public int? PixelHeight { get; set; } + public AssetMetadata Metadata { get; set; } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs index 88d017ba7..b9bc639f4 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Globalization; using MongoDB.Bson.IO; using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter; @@ -142,19 +141,12 @@ namespace Squidex.Infrastructure.MongoDb 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) { - if (value.Offset == TimeSpan.Zero) - { - 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)); - } + bsonWriter.WriteString(value.UtcDateTime.ToIso8601()); } public override void WriteValue(byte[]? value) diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs index 1055a694e..315bf34cd 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.Linq; using MongoDB.Driver; using Squidex.Infrastructure.Queries; diff --git a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs index cbe785d6c..3d7ef6d9a 100644 --- a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -18,6 +18,11 @@ namespace Squidex.Infrastructure return source.Intersect(other).Count() == other.Count; } + public static bool SetEquals(this ICollection source, ICollection other, IEqualityComparer comparer) + { + return source.Intersect(other, comparer).Count() == other.Count; + } + public static IResultList SortSet(this IResultList input, Func idProvider, IReadOnlyList ids) where T : class { return ResultList.Create(input.Total, SortList(input, idProvider, ids)); @@ -162,9 +167,19 @@ namespace Squidex.Infrastructure public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary other, IEqualityComparer keyComparer, IEqualityComparer valueComparer) where TKey : notnull { + if (other == null) + { + return false; + } + + if (dictionary.Count != other.Count) + { + return false; + } + var comparer = new KeyValuePairComparer(keyComparer, valueComparer); - return other != null && dictionary.Count == other.Count && !dictionary.Except(other, comparer).Any(); + return !dictionary.Except(other, comparer).Any(); } public static Dictionary ToDictionary(this IReadOnlyDictionary dictionary) where TKey : notnull diff --git a/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs index 01a5e8184..2672c80de 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs @@ -11,12 +11,14 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +#pragma warning disable IDE0044 // Add readonly modifier + namespace Squidex.Infrastructure.Collections { public class ArrayDictionary : IReadOnlyDictionary where TKey : notnull { private readonly IEqualityComparer keyComparer; - private readonly KeyValuePair[] items; + private KeyValuePair[] items; public TValue this[TKey key] { @@ -66,85 +68,131 @@ namespace Squidex.Infrastructure.Collections this.keyComparer = keyComparer; } - public KeyValuePair[] With(TKey key, TValue value) + public bool IsUnchanged(KeyValuePair[] values) { - var result = new List>(Math.Max(items.Length, 1)); + return ReferenceEquals(values, items); + } - var wasReplaced = false; + public ArrayDictionary With(TKey key, TValue value, IEqualityComparer? valueComparer = null) + { + return With>(key, value, valueComparer); + } - for (var i = 0; i < items.Length; i++) + public TArray With(TKey key, TValue value, IEqualityComparer? valueComparer = null) where TArray : ArrayDictionary + { + var index = IndexOf(key); + + if (index < 0) { - var item = items[i]; + var result = new KeyValuePair[items.Length + 1]; - if (wasReplaced || !keyComparer.Equals(item.Key, key)) - { - result.Add(item); - } - else - { - result.Add(new KeyValuePair(key, value)); - wasReplaced = true; - } + Array.Copy(items, 0, result, 0, items.Length); + + result[^1] = new KeyValuePair(key, value); + + return Create(result); } - if (!wasReplaced) + var existing = items[index].Value; + + if (valueComparer == null || !valueComparer.Equals(value, existing)) { - result.Add(new KeyValuePair(key, value)); + var result = new KeyValuePair[items.Length]; + + Array.Copy(items, 0, result, 0, items.Length); + + result[index] = new KeyValuePair(key, value); + + return Create(result); } - return result.ToArray(); + return Self(); } - public KeyValuePair[] Without(TKey key) + public ArrayDictionary Without(TKey key) { - var result = new List>(Math.Max(items.Length, 1)); + return Without>(key); + } - var wasRemoved = false; + public TArray Without(TKey key) where TArray : ArrayDictionary + { + var index = IndexOf(key); - for (var i = 0; i < items.Length; i++) + if (index < 0) + { + return Self(); + } + + var result = Array.Empty>(); + + if (items.Length > 1) { - var item = items[i]; + result = new KeyValuePair[items.Length - 1]; - if (wasRemoved || !keyComparer.Equals(item.Key, key)) - { - result.Add(item); - } - else - { - wasRemoved = true; - } + var afterIndex = items.Length - index - 1; + + Array.Copy(items, 0, result, 0, index); + Array.Copy(items, index, result, index, afterIndex); } - return result.ToArray(); + return Create(result); } - public bool ContainsKey(TKey key) + private TArray Self() where TArray : ArrayDictionary { - for (var i = 0; i < items.Length; i++) + return (this as TArray)!; + } + + private TArray Create(KeyValuePair[] newItems) where TArray : ArrayDictionary + { + if (ReferenceEquals(items, newItems)) { - if (keyComparer.Equals(items[i].Key, key)) - { - return true; - } + return Self(); } - return false; + var newClone = (TArray)MemberwiseClone(); + + newClone.items = newItems; + + return newClone; + } + + public bool ContainsKey(TKey key) + { + var index = IndexOf(key); + + return index >= 0; } public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + var index = IndexOf(key); + + if (index >= 0) + { + value = items[index].Value; + + return true; + } + else + { + value = default!; + + return false; + } + } + + private int IndexOf(TKey key) { for (var i = 0; i < items.Length; i++) { if (keyComparer.Equals(items[i].Key, key)) { - value = items[i].Value; - return true; + return i; } } - value = default!; - - return false; + return -1; } IEnumerator> IEnumerable>.GetEnumerator() diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs index 72728d84c..72f0e469d 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs @@ -32,12 +32,21 @@ namespace Squidex.Infrastructure.Commands this.store = store; } - protected sealed override void ApplyEvent(Envelope @event) + protected sealed override bool ApplyEvent(Envelope @event, bool isLoading) { var newVersion = Version + 1; - snapshot = OnEvent(@event); - snapshot.Version = newVersion; + var newSnapshot = OnEvent(@event); + + if (!ReferenceEquals(Snapshot, newSnapshot) || isLoading) + { + snapshot = newSnapshot; + snapshot.Version = newVersion; + + return true; + } + + return false; } 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) { - persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot(ApplySnapshot), ApplyEvent); + persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot(ApplySnapshot), x => ApplyEvent(x, true)); return persistence.ReadAsync(); } diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs index 9d6c13d51..a91018e50 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs @@ -73,9 +73,10 @@ namespace Squidex.Infrastructure.Commands @event.SetAggregateId(id); - ApplyEvent(@event); - - uncomittedEvents.Add(@event); + if (ApplyEvent(@event, false)) + { + uncomittedEvents.Add(@event); + } } public IReadOnlyList> GetUncomittedEvents() @@ -206,7 +207,7 @@ namespace Squidex.Infrastructure.Commands protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion); - protected abstract void ApplyEvent(Envelope @event); + protected abstract bool ApplyEvent(Envelope @event, bool isLoading); protected abstract Task ReadAsync(Type type, Guid id); diff --git a/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs index 7d0c30e23..dfd04966e 100644 --- a/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs +++ b/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs @@ -54,17 +54,26 @@ namespace Squidex.Infrastructure.Commands return default!; } - protected sealed override void ApplyEvent(Envelope @event) + protected sealed override bool ApplyEvent(Envelope @event, bool isLoading) { + var newVersion = Version + 1; + var snapshot = OnEvent(@event); - snapshot.Version = Version + 1; - snapshots.Add(snapshot); + if (!ReferenceEquals(Snapshot, snapshot) || isLoading) + { + snapshot.Version = newVersion; + snapshots.Add(snapshot); + + return true; + } + + return false; } 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(); } diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs index 12dfba67d..21b5c48aa 100644 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using Newtonsoft.Json; using Squidex.Infrastructure.Json.Objects; @@ -103,7 +102,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft case JsonToken.Boolean: return JsonValue.Create((bool)reader.Value!); 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: return JsonValue.Create(reader.Value!.ToString()); case JsonToken.Null: diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs b/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs index 34596e2d2..e2180b7f5 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Diagnostics.CodeAnalysis; namespace Squidex.Infrastructure.Json.Objects { @@ -13,6 +14,8 @@ namespace Squidex.Infrastructure.Json.Objects { JsonValueType Type { get; } + bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result); + string ToJsonString(); string ToString(); diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs index efbfffdeb..8f61cb249 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs @@ -8,6 +8,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; namespace Squidex.Infrastructure.Json.Objects @@ -92,5 +94,21 @@ namespace Squidex.Infrastructure.Json.Objects { 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; + } } } diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs index 8d8cfe754..18ea75059 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Diagnostics.CodeAnalysis; namespace Squidex.Infrastructure.Json.Objects { @@ -51,5 +52,12 @@ namespace Squidex.Infrastructure.Json.Objects { return "null"; } + + public bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result) + { + result = null!; + + return false; + } } } diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs index 29dfa2b05..8f523809f 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs @@ -51,7 +51,7 @@ namespace Squidex.Infrastructure.Json.Objects get { return JsonValueType.Array; } } - internal JsonObject() + public JsonObject() { inner = new Dictionary(); } @@ -132,5 +132,12 @@ namespace Squidex.Infrastructure.Json.Objects { 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!); + } } } diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs index 8b0ab1c12..e2c76751e 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Diagnostics.CodeAnalysis; namespace Squidex.Infrastructure.Json.Objects { @@ -49,5 +50,12 @@ namespace Squidex.Infrastructure.Json.Objects { return ToString(); } + + public bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result) + { + result = null!; + + return false; + } } } diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs index dfe024aad..015ad46b9 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs @@ -6,6 +6,8 @@ // ========================================================================== using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using NodaTime; #pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator @@ -14,6 +16,8 @@ namespace Squidex.Infrastructure.Json.Objects { public static class JsonValue { + private static readonly char[] PathSeparators = { '.', '[', ']' }; + public static readonly IJsonValue Empty = new JsonString(string.Empty); public static readonly IJsonValue True = JsonBoolean.True; @@ -132,5 +136,28 @@ namespace Squidex.Infrastructure.Json.Objects return new JsonString(value); } + + public static bool TryGetByPath(this IJsonValue value, string? path, [MaybeNullWhen(false)] out IJsonValue result) + { + return TryGetByPath(value, path?.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries), out result!); + } + + public static bool TryGetByPath(this IJsonValue? value, IEnumerable? path, [MaybeNullWhen(false)] out IJsonValue result) + { + result = value!; + + if (path != null) + { + foreach (var pathSegment in path) + { + if (result == null || !result.TryGet(pathSegment, out result!)) + { + break; + } + } + } + + return result != null && !ReferenceEquals(result, value); + } } } diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs b/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs index 8fe6ffdc0..033147d3d 100644 --- a/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs +++ b/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs @@ -112,6 +112,11 @@ namespace Squidex.Infrastructure.Queries return value != null ? new ClrValue(value, ClrValueType.String, true) : Null; } + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Dynamic, true) : Null; + } + public override string ToString() { if (Value is IList list) diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs b/backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs index 338c9b8a3..d205d5c1a 100644 --- a/backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs +++ b/backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs @@ -10,6 +10,7 @@ namespace Squidex.Infrastructure.Queries public enum ClrValueType { Boolean, + Dynamic, Guid, Double, Instant, diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs b/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs index e7f864cc2..2e127c665 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs @@ -54,6 +54,8 @@ namespace Squidex.Infrastructure.Queries.Json { switch (schema.Type) { + case JsonObjectType.None: + return true; case JsonObjectType.Boolean: return BooleanOperators.Contains(compareOperator); case JsonObjectType.Integer: diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs b/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs index bf3b1d113..8361ebde0 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs @@ -22,6 +22,11 @@ namespace Squidex.Infrastructure.Queries.Json if (parent.Properties.TryGetValue(element, out var p)) { schema = p; + + if (schema.Type == JsonObjectType.None) + { + break; + } } else { diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs index a0318d14c..5c60da153 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Linq; using NJsonSchema; using NodaTime; using NodaTime.Text; @@ -31,6 +32,22 @@ namespace Squidex.Infrastructure.Queries.Json switch (GetType(schema)) { + case JsonObjectType.None: + { + if (value is JsonArray jsonArray) + { + var array = ParseArray(errors, path, jsonArray, TryParseDynamic); + + result = array.Select(x => x?.Value).ToList(); + } + else if (TryParseDynamic(errors, path, value, out var temp)) + { + result = temp; + } + + break; + } + case JsonObjectType.Boolean: { if (value is JsonArray jsonArray) @@ -225,6 +242,52 @@ namespace Squidex.Infrastructure.Queries.Json return false; } + private static bool TryParseDynamic(List errors, PropertyPath path, IJsonValue value, out ClrValue? result) + { + result = null; + + switch (value) + { + case JsonNull _: + return true; + case JsonNumber jsonNumber: + result = jsonNumber.Value; + return true; + case JsonBoolean jsonBoolean: + result = jsonBoolean.Value; + return true; + case JsonString jsonString: + { + if (Guid.TryParse(jsonString.Value, out var guid)) + { + result = guid; + + return true; + } + + foreach (var pattern in InstantPatterns) + { + var parsed = pattern.Parse(jsonString.Value); + + if (parsed.Success) + { + result = parsed.Value; + + return true; + } + } + + result = jsonString.Value; + + return true; + } + } + + errors.Add($"Expected primitive for path '{path}', but got {value.Type}."); + + return false; + } + private static JsonObjectType GetType(JsonSchema schema) { if (schema.Item != null) diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs index 6fdbd3139..3869c6b23 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs @@ -53,6 +53,11 @@ namespace Squidex.Infrastructure.Queries.OData } } + public override ImmutableList Visit(SingleValueOpenPropertyAccessNode nodeIn) + { + return nodeIn.Source.Accept(this).Add(nodeIn.Name); + } + private static string UnescapeEdmField(IEdmNamedElement property) { return property.Name; diff --git a/backend/src/Squidex.Infrastructure/StringExtensions.cs b/backend/src/Squidex.Infrastructure/StringExtensions.cs index 44f16583e..70319cc47 100644 --- a/backend/src/Squidex.Infrastructure/StringExtensions.cs +++ b/backend/src/Squidex.Infrastructure/StringExtensions.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -797,5 +798,10 @@ namespace Squidex.Infrastructure return string.Join(separator, parts.Where(x => !string.IsNullOrWhiteSpace(x))); } + + public static string ToIso8601(this DateTime value) + { + return value.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture); + } } } diff --git a/backend/src/Squidex.Infrastructure/Validation/Not.cs b/backend/src/Squidex.Infrastructure/Validation/Not.cs index 2f5ee4094..f10a78a69 100644 --- a/backend/src/Squidex.Infrastructure/Validation/Not.cs +++ b/backend/src/Squidex.Infrastructure/Validation/Not.cs @@ -71,12 +71,6 @@ namespace Squidex.Infrastructure.Validation return $"{Upper(property)} is not a valid value."; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string New(string type, string property) - { - return $"{Upper(type)} has already this {Lower(property)}."; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string DefinedOr(string property1, string property2) { diff --git a/backend/src/Squidex.Web/Services/UrlGenerator.cs b/backend/src/Squidex.Web/Services/UrlGenerator.cs index ec75ee008..8ffd51a5d 100644 --- a/backend/src/Squidex.Web/Services/UrlGenerator.cs +++ b/backend/src/Squidex.Web/Services/UrlGenerator.cs @@ -7,6 +7,7 @@ using System; using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Entities; @@ -36,7 +37,7 @@ namespace Squidex.Web.Services public string? GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset) { - if (!asset.IsImage) + if (asset.Type != AssetType.Image) { return null; } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs index 0fafa6ba0..d3b31e8d0 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs @@ -14,6 +14,7 @@ using NSwag.Generation; using NSwag.Generation.Processors; using Squidex.Areas.Api.Controllers.Contents.Generator; using Squidex.Areas.Api.Controllers.Rules.Models; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; @@ -70,16 +71,31 @@ namespace Squidex.Areas.Api.Config.OpenApi { settings.TypeMappers = new List { - new PrimitiveTypeMapper(typeof(Instant), schema => + CreateStringMap(JsonFormatStrings.DateTime), + CreateStringMap(), + CreateStringMap(), + CreateStringMap(), + + new PrimitiveTypeMapper(typeof(AssetMetadata), schema => { - schema.Type = JsonObjectType.String; - schema.Format = JsonFormatStrings.DateTime; - }), + schema.Type = JsonObjectType.Object; - new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String), - new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String), - new PrimitiveTypeMapper(typeof(Status), s => s.Type = JsonObjectType.String) + schema.AdditionalPropertiesSchema = new JsonSchema + { + Description = "Any JSON type" + }; + }) }; } + + private static ITypeMapper CreateStringMap(string? format = null) + { + return new PrimitiveTypeMapper(typeof(T), schema => + { + schema.Type = JsonObjectType.String; + + schema.Format = format; + }); + } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index bd06ce91b..6353fe510 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -69,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// Contributor object that needs to be added to the app. /// /// 201 => User assigned to app. - /// 400 => User is already assigned to the app or not found. + /// 400 => User is not found. /// 404 => App not found. /// [HttpPost] @@ -93,7 +93,6 @@ namespace Squidex.Areas.Api.Controllers.Apps /// The id of the contributor. /// /// 200 => User removed from app. - /// 400 => User is not assigned to the app. /// 404 => Contributor or app not found. /// [HttpDelete] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index eb55fb6f2..e105e972c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Assets.Models; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; @@ -126,7 +127,7 @@ namespace Squidex.Areas.Api.Controllers.Assets var handler = new Func(async bodyStream => { - if (asset.IsImage && query.ShouldResize()) + if (asset.Type == AssetType.Image && query.ShouldResize()) { var resizedAsset = $"{asset.Id}_{asset.FileVersion}_{query.Width}_{query.Height}_{query.Mode}"; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 4e4254bf9..990705c9a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -138,7 +138,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpGet] [Route("apps/{app}/assets/{id}/")] - [ProducesResponseType(typeof(AssetsDto), 200)] + [ProducesResponseType(typeof(AssetDto), 200)] [ApiPermission(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAsset(string app, Guid id) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs index ab3b56805..d30363138 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure.Reflection; @@ -29,6 +30,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models /// public HashSet? Tags { get; set; } + /// + /// The asset metadata. + /// + public AssetMetadata? Metadata { get; set; } + public AnnotateAsset ToCommand(Guid id) { return SimpleMapper.Map(this, new AnnotateAsset { AssetId = id }); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index b93767441..899496fb5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Newtonsoft.Json; using NodaTime; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; @@ -60,34 +61,37 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models public string FileType { get; set; } /// - /// The asset tags. + /// The formatted text representation of the metadata. /// - public HashSet? Tags { get; set; } + [Required] + public string MetadataText { get; set; } /// - /// The size of the file in bytes. + /// The asset metadata. /// - public long FileSize { get; set; } + [Required] + public AssetMetadata Metadata { get; set; } /// - /// The version of the file. + /// The asset tags. /// - public long FileVersion { get; set; } + [Required] + public HashSet? Tags { get; set; } /// - /// Determines of the created file is an image. + /// The size of the file in bytes. /// - public bool IsImage { get; set; } + public long FileSize { get; set; } /// - /// The width of the image in pixels if the asset is an image. + /// The version of the file. /// - public int? PixelWidth { get; set; } + public long FileVersion { get; set; } /// - /// The height of the image in pixels if the asset is an image. + /// The type of the asset. /// - public int? PixelHeight { get; set; } + public AssetType Type { get; set; } /// /// The user that has created the schema. @@ -120,7 +124,34 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models /// The metadata. /// [JsonProperty("_meta")] - public AssetMetadata Metadata { get; set; } + public AssetMeta Meta { get; set; } + + /// + /// Determines of the created file is an image. + /// + [Obsolete] + public bool IsImage + { + get { return Type == AssetType.Image; } + } + + /// + /// The width of the image in pixels if the asset is an image. + /// + [Obsolete] + public int? PixelWidth + { + get { return Metadata.GetPixelWidth(); } + } + + /// + /// The height of the image in pixels if the asset is an image. + /// + [Obsolete] + public int? PixelHeight + { + get { return Metadata.GetPixelHeight(); } + } public static AssetDto FromAsset(IEnrichedAssetEntity asset, ApiController controller, string app, bool isDuplicate = false) { @@ -130,7 +161,10 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models if (isDuplicate) { - response.Metadata = new AssetMetadata { IsDuplicate = "true" }; + response.Meta = new AssetMeta + { + IsDuplicate = "true" + }; } return CreateLinks(response, controller, app); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMeta.cs similarity index 93% rename from backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMeta.cs index 71f8e9065..a29693a09 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMeta.cs @@ -7,7 +7,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models { - public sealed class AssetMetadata + public sealed class AssetMeta { /// /// Indicates whether the asset is a duplicate. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index af2b24346..af376da14 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -156,7 +156,6 @@ namespace Squidex.Areas.Api.Controllers.Rules /// The id of the rule to enable. /// /// 200 => Rule enabled. - /// 400 => Rule already enabled. /// 404 => Rule or app not found. /// [HttpPut] @@ -180,7 +179,6 @@ namespace Squidex.Areas.Api.Controllers.Rules /// The id of the rule to disable. /// /// 200 => Rule disabled. - /// 400 => Rule already disabled. /// 404 => Rule or app not found. /// [HttpPut] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index 6671acc21..475ebdb02 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -217,7 +217,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The id of the field to lock. /// /// 200 => Schema field shown. - /// 400 => Schema field already locked. /// 404 => Schema, field or app not found. /// /// @@ -246,7 +245,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The id of the field to lock. /// /// 200 => Schema field hidden. - /// 400 => Schema field already hidden. /// 404 => Field, schema, or app not found. /// /// @@ -274,7 +272,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The id of the field to hide. /// /// 200 => Schema field hidden. - /// 400 => Schema field already hidden. /// 404 => Schema, field or app not found. /// /// @@ -303,7 +300,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The id of the field to hide. /// /// 200 => Schema field hidden. - /// 400 => Schema field already hidden. /// 404 => Field, schema, or app not found. /// /// @@ -331,7 +327,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The id of the field to show. /// /// 200 => Schema field shown. - /// 400 => Schema field already visible. /// 404 => Schema, field or app not found. /// /// @@ -360,7 +355,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The id of the field to show. /// /// 200 => Schema field shown. - /// 400 => Schema field already visible. /// 404 => Schema, field or app not found. /// /// @@ -388,7 +382,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The id of the field to enable. /// /// 200 => Schema field enabled. - /// 400 => Schema field already enabled. /// 404 => Schema, field or app not found. /// /// @@ -417,7 +410,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The id of the field to enable. /// /// 200 => Schema field enabled. - /// 400 => Schema field already enabled. /// 404 => Schema, field or app not found. /// /// @@ -445,7 +437,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The id of the field to disable. /// /// 200 => Schema field disabled. - /// 400 => Schema field already disabled. /// 404 => Schema, field or app not found. /// /// @@ -474,7 +465,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The id of the field to disable. /// /// 200 => Schema field disabled. - /// 400 => Schema field already disabled. /// 404 => Schema, field or app not found. /// /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 14154d376..758cd584c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -256,7 +256,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The name of the schema to publish. /// /// 200 => Schema has been published. - /// 400 => Schema is already published. /// 404 => Schema or app not found. /// [HttpPut] @@ -280,7 +279,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The name of the schema to unpublish. /// /// 200 => Schema has been unpublished. - /// 400 => Schema is not published. /// 404 => Schema or app not found. /// [HttpPut] diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index 6e45ccc55..98938fe58 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -12,9 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using MongoDB.Driver.GridFS; using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Queries; -using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets.ImageSharp; @@ -49,10 +47,10 @@ namespace Squidex.Config.Domain .As().As(); services.AddSingletonAs() - .As>(); + .As(); - services.AddSingletonAs() - .As>(); + services.AddSingletonAs() + .As(); } public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config) diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index c5096117c..a3271e7c1 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -15,7 +15,6 @@ using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Comments; -using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Indexes; diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs index 843c2e424..20e44531c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs @@ -58,6 +58,14 @@ namespace Squidex.Domain.Apps.Core.Model.Apps clients_1["1"].Should().BeEquivalentTo(new AppClient("new-name", "my-secret", Role.Editor)); } + [Fact] + public void Should_return_same_clients_if_client_is_updated_with_the_same_values() + { + var clients_1 = clients_0.Rename("2", "2"); + + Assert.Same(clients_0, clients_1); + } + [Fact] public void Should_return_same_clients_if_client_to_rename_not_found() { @@ -95,7 +103,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var clients_1 = clients_0.Revoke("2"); - Assert.NotEmpty(clients_1); + Assert.Same(clients_0, clients_1); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs index faf082a3d..495ae9c21 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs @@ -35,6 +35,15 @@ namespace Squidex.Domain.Apps.Core.Model.Apps Assert.Equal(Role.Owner, contributors_2["1"]); } + [Fact] + public void Should_return_same_contributors_if_contributor_is_updated_with_same_role() + { + var contributors_1 = contributors_0.Assign("1", Role.Developer); + var contributors_2 = contributors_1.Assign("1", Role.Developer); + + Assert.Same(contributors_1, contributors_2); + } + [Fact] public void Should_remove_contributor() { @@ -49,7 +58,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var contributors_1 = contributors_0.Remove("2"); - Assert.Empty(contributors_1); + Assert.Same(contributors_0, contributors_1); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternJsonTests.cs index 9ffe845bc..0b2f79947 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternJsonTests.cs @@ -26,9 +26,8 @@ namespace Squidex.Domain.Apps.Core.Model.Apps patterns = patterns.Add(guid1, "Name1", "Pattern1", "Default"); patterns = patterns.Add(guid2, "Name2", "Pattern2", "Default"); patterns = patterns.Add(guid3, "Name3", "Pattern3", "Default"); - patterns = patterns.Update(guid2, "Name2 Update", "Pattern2 Update", "Default2"); - + patterns = patterns.Update(guid3, "Name3 Update", "Pattern3 Update", "Default3"); patterns = patterns.Remove(guid1); var serialized = patterns.SerializeAndDeserialize(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs index de159e090..557d216ae 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs @@ -50,7 +50,15 @@ namespace Squidex.Domain.Apps.Core.Model.Apps } [Fact] - public void Should_return_same_patterns_if_pattern_not_found() + public void Should_return_same_patterns_if_pattern_is_updated_with_same_values() + { + var patterns_1 = patterns_0.Update(firstId, "Default", "Default Pattern", "Message"); + + Assert.Same(patterns_0, patterns_1); + } + + [Fact] + public void Should_return_same_patterns_if_pattern_to_update_not_found() { var patterns_1 = patterns_0.Update(id, "NewPattern", "NewPattern", "Message"); @@ -70,7 +78,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var patterns_1 = patterns_0.Remove(id); - Assert.NotEmpty(patterns_1); + Assert.Same(patterns_0, patterns_1); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs index f1e995112..b66614b54 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs @@ -117,6 +117,19 @@ namespace Squidex.Domain.Apps.Core.Model.Apps Assert.Equal(Language.IT, config_3.Master.Language); } + [Fact] + public void Should_return_same_languages_if_master_language_is_already_master() + { + var config_0 = LanguagesConfig.Build(Language.DE); + + var config_1 = config_0.Set(Language.UK); + var config_2 = config_1.Set(Language.IT); + var config_3 = config_2.MakeMaster(Language.IT); + var config_4 = config_3.MakeMaster(Language.IT); + + Assert.Same(config_3, config_4); + } + [Fact] public void Should_throw_exception_if_language_to_make_master_is_not_found() { @@ -168,11 +181,12 @@ namespace Squidex.Domain.Apps.Core.Model.Apps } [Fact] - public void Should_not_throw_exception_if_language_to_remove_is_not_found() + public void Should_do_nothing_if_language_to_remove_is_not_found() { var config_0 = LanguagesConfig.Build(Language.DE); + var config_1 = config_0.Remove(Language.EN); - config_0.Remove(Language.EN); + Assert.Equal(config_0, config_1); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index b65f88824..f0b144b15 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs @@ -67,6 +67,14 @@ namespace Squidex.Domain.Apps.Core.Model.Apps roles_1[firstRole].Should().BeEquivalentTo(new Role(firstRole, new PermissionSet("P1", "P2"))); } + [Fact] + public void Should_return_same_roles_if_role_is_updated_with_same_values() + { + var roles_1 = roles_0.Update(firstRole); + + Assert.Same(roles_0, roles_1); + } + [Fact] public void Should_return_same_roles_if_role_not_found() { @@ -88,7 +96,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var roles_1 = roles_0.Remove(role); - Assert.True(roles_1.CustomCount > 0); + Assert.Same(roles_0, roles_1); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs new file mode 100644 index 000000000..0c438d84a --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs @@ -0,0 +1,170 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Assets; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Assets +{ + public class AssetMetadataTests + { + [Fact] + public void Should_return_pixel_infos_if_found() + { + var sut = + new AssetMetadata() + .SetPixelWidth(800) + .SetPixelHeight(600); + + Assert.Equal(800, sut.GetPixelWidth()); + Assert.Equal(600, sut.GetPixelHeight()); + } + + [Fact] + public void Should_return_null_if_pixel_infos_not_found() + { + var sut = new AssetMetadata(); + + Assert.Null(sut.GetPixelWidth()); + Assert.Null(sut.GetPixelHeight()); + } + + [Fact] + public void Should_return_self_if_path_is_empty() + { + var sut = new AssetMetadata(); + + var found = sut.TryGetByPath(string.Empty, out var result); + + Assert.False(found); + Assert.Same(sut, result); + } + + [Fact] + public void Should_return_plain_value_if_found() + { + var sut = new AssetMetadata().SetPixelWidth(800); + + var found = sut.TryGetByPath("pixelWidth", out var result); + + Assert.True(found); + Assert.Equal(JsonValue.Create(800), result); + } + + [Fact] + public void Should_return_null_if_not_found() + { + var sut = new AssetMetadata().SetPixelWidth(800); + + var found = sut.TryGetByPath("pixelHeight", out var result); + + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void Should_return_nested_value_if_found() + { + var sut = new AssetMetadata + { + ["meta"] = + JsonValue.Object() + .Add("nested1", + JsonValue.Object() + .Add("nested2", 12)) + }; + + var found = sut.TryGetByPath("meta.nested1.nested2", out var result); + + Assert.True(found); + Assert.Equal(JsonValue.Create(12), result); + } + + [Fact] + public void Should_get_string_if_found() + { + var value = "Hello"; + + var sut = new AssetMetadata + { + ["string"] = JsonValue.Create(value) + }; + + var found = sut.TryGetString("string", out var result); + + Assert.True(found); + Assert.Equal(value, result); + } + + [Fact] + public void Should_get_null_if_property_is_not_string() + { + var sut = new AssetMetadata + { + ["string"] = JsonValue.Create(12) + }; + + var found = sut.TryGetString("string", out var result); + + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void Should_get_null_if_string_property_not_found() + { + var sut = new AssetMetadata(); + + var found = sut.TryGetString("other", out var result); + + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void Should_get_number_if_found() + { + var value = 12.5; + + var sut = new AssetMetadata + { + ["number"] = JsonValue.Create(value) + }; + + var found = sut.TryGetNumber("number", out var result); + + Assert.True(found); + Assert.Equal(value, result); + } + + [Fact] + public void Should_get_null_if_property_is_not_number() + { + var sut = new AssetMetadata + { + ["number"] = JsonValue.Create(true) + }; + + var found = sut.TryGetNumber("number", out var result); + + Assert.False(found); + Assert.Equal(0, result); + } + + [Fact] + public void Should_get_null_if_number_property_not_found() + { + var sut = new AssetMetadata(); + + var found = sut.TryGetNumber("other", out var result); + + Assert.False(found); + Assert.Equal(0, result); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs index e1c398a6e..a1149126c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs @@ -83,8 +83,14 @@ namespace Squidex.Domain.Apps.Core.Model.Rules var rule_2 = rule_1.Enable(); var rule_3 = rule_2.Enable(); + Assert.NotSame(rule_1, rule_2); + Assert.False(rule_1.IsEnabled); + + Assert.True(rule_2.IsEnabled); Assert.True(rule_3.IsEnabled); + + Assert.Same(rule_2, rule_3); } [Fact] @@ -93,27 +99,41 @@ namespace Squidex.Domain.Apps.Core.Model.Rules var rule_1 = rule_0.Disable(); var rule_2 = rule_1.Disable(); - Assert.True(rule_0.IsEnabled); + Assert.NotSame(rule_0, rule_1); + + Assert.False(rule_1.IsEnabled); Assert.False(rule_2.IsEnabled); + + Assert.Same(rule_1, rule_2); } [Fact] public void Should_replace_name_when_renaming() { var rule_1 = rule_0.Rename("MyName"); + var rule_2 = rule_1.Rename("MyName"); + + Assert.NotSame(rule_0, rule_1); Assert.Equal("MyName", rule_1.Name); + Assert.Equal("MyName", rule_2.Name); + Assert.Same(rule_1, rule_2); } [Fact] public void Should_replace_trigger_when_updating() { - var newTrigger = new ContentChangedTriggerV2(); + var newTrigger1 = new ContentChangedTriggerV2 { HandleAll = true }; + var newTrigger2 = new ContentChangedTriggerV2 { HandleAll = true }; - var rule_1 = rule_0.Update(newTrigger); + var rule_1 = rule_0.Update(newTrigger1); + var rule_2 = rule_1.Update(newTrigger2); - Assert.NotSame(newTrigger, rule_0.Trigger); - Assert.Same(newTrigger, rule_1.Trigger); + Assert.NotSame(rule_0.Action, newTrigger1); + Assert.NotSame(rule_0, rule_1); + Assert.Same(newTrigger1, rule_1.Trigger); + Assert.Same(newTrigger1, rule_2.Trigger); + Assert.Same(rule_1, rule_2); } [Fact] @@ -125,12 +145,17 @@ namespace Squidex.Domain.Apps.Core.Model.Rules [Fact] public void Should_replace_action_when_updating() { - var newAction = new TestAction1(); + var newAction1 = new TestAction1 { Property = "NewValue" }; + var newAction2 = new TestAction1 { Property = "NewValue" }; - var rule_1 = rule_0.Update(newAction); + var rule_1 = rule_0.Update(newAction1); + var rule_2 = rule_1.Update(newAction2); - Assert.NotSame(newAction, rule_0.Action); - Assert.Same(newAction, rule_1.Action); + Assert.NotSame(rule_0.Action, newAction1); + Assert.NotSame(rule_0, rule_1); + Assert.Same(newAction1, rule_1.Action); + Assert.Same(newAction1, rule_2.Action); + Assert.Same(rule_1, rule_2); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs index a5422097f..c2f29379e 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs @@ -55,7 +55,9 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var parent_3 = parent_2.UpdateField(1, f => f.Hide()); Assert.False(parent_1.FieldsById[1].IsHidden); + Assert.True(parent_2.FieldsById[1].IsHidden); Assert.True(parent_3.FieldsById[1].IsHidden); + Assert.Same(parent_2, parent_3); } [Fact] @@ -76,7 +78,9 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var parent_4 = parent_3.UpdateField(1, f => f.Show()); Assert.True(parent_2.FieldsById[1].IsHidden); + Assert.False(parent_3.FieldsById[1].IsHidden); Assert.False(parent_4.FieldsById[1].IsHidden); + Assert.Same(parent_3, parent_4); } [Fact] @@ -96,7 +100,9 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var parent_3 = parent_2.UpdateField(1, f => f.Disable()); Assert.False(parent_1.FieldsById[1].IsDisabled); + Assert.True(parent_2.FieldsById[1].IsDisabled); Assert.True(parent_3.FieldsById[1].IsDisabled); + Assert.Same(parent_2, parent_3); } [Fact] @@ -117,7 +123,9 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var parent_4 = parent_3.UpdateField(1, f => f.Enable()); Assert.True(parent_2.FieldsById[1].IsDisabled); + Assert.False(parent_3.FieldsById[1].IsDisabled); Assert.False(parent_4.FieldsById[1].IsDisabled); + Assert.Same(parent_3, parent_4); } [Fact] @@ -131,13 +139,23 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas [Fact] public void Should_update_field() { - var properties = new NumberFieldProperties(); + var properties1 = new NumberFieldProperties + { + MinValue = 10 + }; + var properties2 = new NumberFieldProperties + { + MinValue = 10 + }; var parent_1 = parent_0.AddField(CreateField(1)); - var parent_2 = parent_1.UpdateField(1, f => f.Update(properties)); + var parent_2 = parent_1.UpdateField(1, f => f.Update(properties1)); + var parent_3 = parent_2.UpdateField(1, f => f.Update(properties2)); - Assert.NotSame(properties, parent_1.FieldsById[1].RawProperties); - Assert.Same(properties, parent_2.FieldsById[1].RawProperties); + Assert.NotSame(properties1, parent_1.FieldsById[1].RawProperties); + Assert.Same(properties1, parent_2.FieldsById[1].RawProperties); + Assert.Same(properties1, parent_3.FieldsById[1].RawProperties); + Assert.Same(parent_2, parent_3); } [Fact] @@ -161,8 +179,11 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas { var parent_1 = parent_0.AddField(CreateField(1)); var parent_2 = parent_1.DeleteField(1); + var parent_3 = parent_2.DeleteField(1); Assert.Empty(parent_2.FieldsById); + Assert.Empty(parent_3.FieldsById); + Assert.Same(parent_2, parent_3); } [Fact] @@ -184,8 +205,11 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var parent_2 = parent_1.AddField(field2); var parent_3 = parent_2.AddField(field3); var parent_4 = parent_3.ReorderFields(new List { 3, 2, 1 }); + var parent_5 = parent_4.ReorderFields(new List { 3, 2, 1 }); Assert.Equal(new List { field3, field2, field1 }, parent_4.Fields.ToList()); + Assert.Equal(new List { field3, field2, field1 }, parent_5.Fields.ToList()); + Assert.Same(parent_4, parent_5); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs index 031cfc7c1..2079c1620 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs @@ -35,12 +35,16 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas [Fact] public void Should_update_schema() { - var properties = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; + var properties1 = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; + var properties2 = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; - var schema_1 = schema_0.Update(properties); + var schema_1 = schema_0.Update(properties1); + var schema_2 = schema_1.Update(properties2); - Assert.NotSame(properties, schema_0.Properties); - Assert.Same(properties, schema_1.Properties); + Assert.NotSame(properties1, schema_0.Properties); + Assert.Same(properties1, schema_1.Properties); + Assert.Same(properties1, schema_2.Properties); + Assert.Same(schema_1, schema_2); } [Fact] @@ -79,7 +83,9 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var schema_3 = schema_2.UpdateField(1, f => f.Hide()); Assert.False(schema_1.FieldsById[1].IsHidden); + Assert.True(schema_2.FieldsById[1].IsHidden); Assert.True(schema_3.FieldsById[1].IsHidden); + Assert.Same(schema_2, schema_3); } [Fact] @@ -100,7 +106,9 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var schema_4 = schema_3.UpdateField(1, f => f.Show()); Assert.True(schema_2.FieldsById[1].IsHidden); + Assert.False(schema_3.FieldsById[1].IsHidden); Assert.False(schema_4.FieldsById[1].IsHidden); + Assert.Same(schema_3, schema_4); } [Fact] @@ -120,7 +128,9 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var schema_3 = schema_2.UpdateField(1, f => f.Disable()); Assert.False(schema_1.FieldsById[1].IsDisabled); + Assert.True(schema_2.FieldsById[1].IsDisabled); Assert.True(schema_3.FieldsById[1].IsDisabled); + Assert.Same(schema_2, schema_3); } [Fact] @@ -141,7 +151,9 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var schema_4 = schema_3.UpdateField(1, f => f.Enable()); Assert.True(schema_2.FieldsById[1].IsDisabled); + Assert.False(schema_3.FieldsById[1].IsDisabled); Assert.False(schema_4.FieldsById[1].IsDisabled); + Assert.Same(schema_3, schema_4); } [Fact] @@ -161,7 +173,9 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var schema_3 = schema_2.UpdateField(1, f => f.Lock()); Assert.False(schema_1.FieldsById[1].IsLocked); + Assert.True(schema_2.FieldsById[1].IsLocked); Assert.True(schema_3.FieldsById[1].IsLocked); + Assert.Same(schema_2, schema_3); } [Fact] @@ -175,13 +189,23 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas [Fact] public void Should_update_field() { - var properties = new NumberFieldProperties(); + var properties1 = new NumberFieldProperties + { + MinValue = 10 + }; + var properties2 = new NumberFieldProperties + { + MinValue = 10 + }; var schema_1 = schema_0.AddField(CreateField(1)); - var schema_2 = schema_1.UpdateField(1, f => f.Update(properties)); + var schema_2 = schema_1.UpdateField(1, f => f.Update(properties1)); + var schema_3 = schema_2.UpdateField(1, f => f.Update(properties2)); - Assert.NotSame(properties, schema_1.FieldsById[1].RawProperties); - Assert.Same(properties, schema_2.FieldsById[1].RawProperties); + Assert.NotSame(properties1, schema_1.FieldsById[1].RawProperties); + Assert.Same(properties1, schema_2.FieldsById[1].RawProperties); + Assert.Same(properties1, schema_3.FieldsById[1].RawProperties); + Assert.Same(schema_2, schema_3); } [Fact] @@ -205,8 +229,11 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas { var schema_1 = schema_0.AddField(CreateField(1)); var schema_2 = schema_1.DeleteField(1); + var schema_3 = schema_2.DeleteField(1); Assert.Empty(schema_2.FieldsById); + Assert.Empty(schema_3.FieldsById); + Assert.Same(schema_2, schema_3); } [Fact] @@ -237,9 +264,12 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas public void Should_publish_schema() { var schema_1 = schema_0.Publish(); + var schema_2 = schema_1.Publish(); Assert.False(schema_0.IsPublished); Assert.True(schema_1.IsPublished); + Assert.True(schema_2.IsPublished); + Assert.Same(schema_1, schema_2); } [Fact] @@ -247,9 +277,12 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas { var schema_1 = schema_0.Publish(); var schema_2 = schema_1.Unpublish(); + var schema_3 = schema_2.Unpublish(); Assert.True(schema_1.IsPublished); Assert.False(schema_2.IsPublished); + Assert.False(schema_3.IsPublished); + Assert.Same(schema_2, schema_3); } [Fact] @@ -263,8 +296,11 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var schema_2 = schema_1.AddField(field2); var schema_3 = schema_2.AddField(field3); var schema_4 = schema_3.ReorderFields(new List { 3, 2, 1 }); + var schema_5 = schema_4.ReorderFields(new List { 3, 2, 1 }); Assert.Equal(new List { field3, field2, field1 }, schema_4.Fields.ToList()); + Assert.Equal(new List { field3, field2, field1 }, schema_5.Fields.ToList()); + Assert.Same(schema_4, schema_5); } [Fact] @@ -295,54 +331,77 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas public void Should_change_category() { var schema_1 = schema_0.ChangeCategory("Category"); + var schema_2 = schema_1.ChangeCategory("Category"); Assert.Equal("Category", schema_1.Category); + Assert.Equal("Category", schema_2.Category); + Assert.Same(schema_1, schema_2); } [Fact] public void Should_set_list_fields() { - var schema_1 = schema_0.ConfigureFieldsInLists("1"); + var schema_1 = schema_0.ConfigureFieldsInLists("2"); + var schema_2 = schema_1.ConfigureFieldsInLists("2"); - Assert.Equal(new[] { "1" }, schema_1.FieldsInLists); + Assert.Equal(new[] { "2" }, schema_1.FieldsInLists); + Assert.Equal(new[] { "2" }, schema_2.FieldsInLists); + Assert.Same(schema_1, schema_2); } [Fact] public void Should_set_reference_fields() { var schema_1 = schema_0.ConfigureFieldsInReferences("2"); + var schema_2 = schema_1.ConfigureFieldsInReferences("2"); Assert.Equal(new[] { "2" }, schema_1.FieldsInReferences); + Assert.Equal(new[] { "2" }, schema_2.FieldsInReferences); + Assert.Same(schema_1, schema_2); } [Fact] public void Should_configure_scripts() { - var scripts = new SchemaScripts + var scripts1 = new SchemaScripts + { + Query = "" + }; + var scripts2 = new SchemaScripts { Query = "" }; - var schema_1 = schema_0.ConfigureScripts(scripts); - - Assert.Equal(scripts, schema_1.Scripts); + var schema_1 = schema_0.ConfigureScripts(scripts1); + var schema_2 = schema_1.ConfigureScripts(scripts2); Assert.Equal("", schema_1.Scripts.Query); + + Assert.Equal(scripts1, schema_1.Scripts); + Assert.Equal(scripts1, schema_2.Scripts); + Assert.Same(schema_1, schema_2); } [Fact] public void Should_configure_preview_urls() { - var urls = new Dictionary + var urls1 = new Dictionary + { + ["web"] = "Url" + }; + var urls2 = new Dictionary { ["web"] = "Url" }; - var schema_1 = schema_0.ConfigurePreviewUrls(urls); - - Assert.Equal(urls, schema_1.PreviewUrls); + var schema_1 = schema_0.ConfigurePreviewUrls(urls1); + var schema_2 = schema_1.ConfigurePreviewUrls(urls2); Assert.Equal("Url", schema_1.PreviewUrls["web"]); + + Assert.Equal(urls1, schema_1.PreviewUrls); + Assert.Equal(urls1, schema_2.PreviewUrls); + Assert.Same(schema_1, schema_2); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs index 950383d72..84a27e916 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs @@ -11,7 +11,6 @@ using Squidex.Domain.Apps.Core.EventSynchronization; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; using Xunit; namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization @@ -19,7 +18,6 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization public class SchemaSynchronizerTests { private readonly Func idGenerator; - private readonly IJsonSerializer jsonSerializer = TestUtils.DefaultSerializer; private readonly NamedId stringId = NamedId.Of(13L, "my-value"); private readonly NamedId nestedId = NamedId.Of(141L, "my-value"); private readonly NamedId arrayId = NamedId.Of(14L, "11-array"); @@ -39,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization var targetSchema = (Schema?)null; - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new SchemaDeleted() @@ -56,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization new Schema("target") .ChangeCategory("Category"); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new SchemaCategoryChanged { Name = "Category" } @@ -77,7 +75,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization var targetSchema = new Schema("target").ConfigureScripts(scripts); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new SchemaScriptsConfigured { Scripts = scripts } @@ -99,7 +97,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization new Schema("target") .ConfigurePreviewUrls(previewUrls); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new SchemaPreviewUrlsConfigured { PreviewUrls = previewUrls } @@ -116,7 +114,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization new Schema("target") .Publish(); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new SchemaPublished() @@ -133,7 +131,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization var targetSchema = new Schema("target"); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new SchemaUnpublished() @@ -151,7 +149,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization new Schema("target") .ConfigureFieldsInLists("2", "1"); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new SchemaUIFieldsConfigured { FieldsInLists = new FieldNames("2", "1") } @@ -169,7 +167,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization new Schema("target") .ConfigureFieldsInReferences("2", "1"); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new SchemaUIFieldsConfigured { FieldsInReferences = new FieldNames("2", "1") } @@ -188,7 +186,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization new Schema("target") .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldDeleted { FieldId = nestedId, ParentFieldId = arrayId } @@ -205,7 +203,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization var targetSchema = new Schema("target"); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldDeleted { FieldId = stringId } @@ -227,7 +225,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f .AddString(nestedId.Id, nestedId.Name, properties)); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldUpdated { Properties = properties, FieldId = nestedId, ParentFieldId = arrayId } @@ -247,7 +245,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization new Schema("target") .AddString(stringId.Id, stringId.Name, Partitioning.Invariant, properties); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldUpdated { Properties = properties, FieldId = stringId } @@ -268,7 +266,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(nestedId.Id, nestedId.Name)) .LockField(nestedId.Id, arrayId.Id); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldLocked { FieldId = nestedId, ParentFieldId = arrayId } @@ -287,7 +285,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) .LockField(stringId.Id); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldLocked { FieldId = stringId } @@ -308,7 +306,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(nestedId.Id, nestedId.Name)) .HideField(nestedId.Id, arrayId.Id); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldHidden { FieldId = nestedId, ParentFieldId = arrayId } @@ -327,7 +325,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) .HideField(stringId.Id); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldHidden { FieldId = stringId } @@ -348,7 +346,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f .AddString(nestedId.Id, nestedId.Name)); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldShown { FieldId = nestedId, ParentFieldId = arrayId } @@ -367,7 +365,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization new Schema("target") .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldShown { FieldId = stringId } @@ -388,7 +386,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(nestedId.Id, nestedId.Name)) .DisableField(nestedId.Id, arrayId.Id); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldDisabled { FieldId = nestedId, ParentFieldId = arrayId } @@ -407,7 +405,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) .DisableField(stringId.Id); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldDisabled { FieldId = stringId } @@ -428,7 +426,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f .AddString(nestedId.Id, nestedId.Name)); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldEnabled { FieldId = nestedId, ParentFieldId = arrayId } @@ -447,7 +445,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization new Schema("target") .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldEnabled { FieldId = stringId } @@ -465,7 +463,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) .HideField(stringId.Id); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); var createdId = NamedId.Of(50L, stringId.Name); @@ -486,7 +484,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization new Schema("target") .AddTags(stringId.Id, stringId.Name, Partitioning.Invariant); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); var createdId = NamedId.Of(50L, stringId.Name); @@ -507,7 +505,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization new Schema("target") .AddString(stringId.Id, stringId.Name, Partitioning.Language); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); var createdId = NamedId.Of(50L, stringId.Name); @@ -529,7 +527,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(nestedId.Id, nestedId.Name)) .HideField(nestedId.Id, arrayId.Id); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); var id1 = NamedId.Of(50L, arrayId.Name); var id2 = NamedId.Of(51L, stringId.Name); @@ -559,7 +557,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(1, "f2") .AddString(2, "f1")); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new SchemaFieldsReordered { FieldIds = new List { 11, 10 }, ParentFieldId = arrayId } @@ -579,7 +577,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(1, "f2", Partitioning.Invariant) .AddString(2, "f1", Partitioning.Invariant); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new SchemaFieldsReordered { FieldIds = new List { 11, 10 } } @@ -599,7 +597,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(1, "f3", Partitioning.Invariant) .AddString(2, "f1", Partitioning.Invariant); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldDeleted { FieldId = NamedId.Of(11L, "f2") }, @@ -622,7 +620,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization .AddString(2, "f3", Partitioning.Invariant) .AddString(3, "f2", Partitioning.Invariant); - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); events.ShouldHaveSameEvents( new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs index 4e05cdcee..5e557e095 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Infrastructure.Collections; @@ -39,6 +40,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent public int? PixelWidth { get; set; } public int? PixelHeight { get; set; } + + public AssetMetadata Metadata { get; set; } + + public AssetType Type { get; set; } } private readonly AssetInfo document = new AssetInfo @@ -46,9 +51,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent AssetId = Guid.NewGuid(), FileName = "MyDocument.pdf", FileSize = 1024 * 4, - IsImage = false, - PixelWidth = null, - PixelHeight = null + Type = AssetType.Unknown }; private readonly AssetInfo image1 = new AssetInfo @@ -56,9 +59,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent AssetId = Guid.NewGuid(), FileName = "MyImage.png", FileSize = 1024 * 8, - IsImage = true, - PixelWidth = 800, - PixelHeight = 600 + Type = AssetType.Image, + Metadata = + new AssetMetadata() + .SetPixelWidth(800) + .SetPixelHeight(600) }; private readonly AssetInfo image2 = new AssetInfo @@ -66,9 +71,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent AssetId = Guid.NewGuid(), FileName = "MyImage.png", FileSize = 1024 * 8, - IsImage = true, - PixelWidth = 800, - PixelHeight = 600 + Type = AssetType.Image, + Metadata = + new AssetMetadata() + .SetPixelWidth(800) + .SetPixelHeight(600) }; private readonly ValidationContext ctx; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs index 01adb340b..277bc77b8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs @@ -86,9 +86,9 @@ namespace Squidex.Domain.Apps.Entities.Apps { var command = new CreateApp { Name = AppName, Actor = Actor, AppId = AppId }; - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(AppName, sut.Snapshot.Name); @@ -96,7 +96,6 @@ namespace Squidex.Domain.Apps.Entities.Apps .ShouldHaveSameEvents( CreateEvent(new AppCreated { Name = AppName }), CreateEvent(new AppContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }), - CreateEvent(new AppLanguageAdded { Language = Language.EN }), CreateEvent(new AppPatternAdded { PatternId = patternId1, Name = "Number", Pattern = "[0-9]" }), CreateEvent(new AppPatternAdded { PatternId = patternId2, Name = "Numbers", Pattern = "[0-9]*" }) ); @@ -109,9 +108,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal("my-label", sut.Snapshot.Label); Assert.Equal("my-description", sut.Snapshot.Description); @@ -129,9 +128,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal("image/png", sut.Snapshot.Image!.MimeType); @@ -147,10 +146,11 @@ namespace Squidex.Domain.Apps.Entities.Apps var command = new RemoveAppImage(); await ExecuteCreateAsync(); + await ExecuteUploadImage(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Null(sut.Snapshot.Image); @@ -170,9 +170,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - Assert.True(result.Value is PlanChangedResult); + Assert.True(result is PlanChangedResult); Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); @@ -196,9 +196,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteChangePlanAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - Assert.True(result.Value is PlanResetResult); + Assert.True(result is PlanResetResult); Assert.Null(sut.Snapshot.Plan); @@ -218,9 +218,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); + result.ShouldBeEquivalent2(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); Assert.Null(sut.Snapshot.Plan); } @@ -232,9 +232,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(5)); + result.ShouldBeEquivalent2(new EntitySavedResult(4)); A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) .MustNotHaveHappened(); @@ -247,9 +247,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]); @@ -267,9 +267,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAssignContributorAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]); @@ -287,9 +287,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAssignContributorAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); @@ -306,9 +306,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); @@ -326,9 +326,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAttachClientAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); @@ -347,9 +347,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAttachClientAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); @@ -366,9 +366,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.NotEmpty(sut.Snapshot.Workflows); @@ -386,9 +386,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAddWorkflowAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.NotEmpty(sut.Snapshot.Workflows); @@ -406,9 +406,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAddWorkflowAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Empty(sut.Snapshot.Workflows); @@ -425,9 +425,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); @@ -445,9 +445,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAddLanguageAsync(Language.DE); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.False(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); @@ -465,9 +465,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAddLanguageAsync(Language.DE); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); @@ -484,9 +484,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(1, sut.Snapshot.Roles.CustomCount); @@ -504,9 +504,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAddRoleAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(0, sut.Snapshot.Roles.CustomCount); @@ -524,9 +524,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAddRoleAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -541,9 +541,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(initialPatterns.Count + 1, sut.Snapshot.Patterns.Count); @@ -561,9 +561,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAddPatternAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(initialPatterns.Count, sut.Snapshot.Patterns.Count); @@ -581,9 +581,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); await ExecuteAddPatternAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -598,9 +598,9 @@ namespace Squidex.Domain.Apps.Entities.Apps await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(5)); + result.ShouldBeEquivalent2(new EntitySavedResult(4)); LastEvents .ShouldHaveSameEvents( @@ -611,49 +611,76 @@ namespace Squidex.Domain.Apps.Entities.Apps .MustHaveHappened(); } - private Task ExecuteAddPatternAsync() + private Task ExecuteCreateAsync() { - return sut.ExecuteAsync(CreateCommand(new AddPattern { PatternId = patternId3, Name = "Name", Pattern = ".*" })); + return PublishAsync(new CreateApp { Name = AppName }); } - private Task ExecuteCreateAsync() + private Task ExecuteUploadImage() + { + return PublishAsync(new UploadAppImage { File = new AssetFile("image.png", "image/png", 100, () => new MemoryStream()) }); + } + + private Task ExecuteAddPatternAsync() { - return sut.ExecuteAsync(CreateCommand(new CreateApp { Name = AppName })); + return PublishAsync(new AddPattern { PatternId = patternId3, Name = "Name", Pattern = ".*" }); } private Task ExecuteAssignContributorAsync() { - return sut.ExecuteAsync(CreateCommand(new AssignContributor { ContributorId = contributorId, Role = Role.Editor })); + return PublishAsync(new AssignContributor { ContributorId = contributorId, Role = Role.Editor }); } private Task ExecuteAttachClientAsync() { - return sut.ExecuteAsync(CreateCommand(new AttachClient { Id = clientId })); + return PublishAsync(new AttachClient { Id = clientId }); } private Task ExecuteAddRoleAsync() { - return sut.ExecuteAsync(CreateCommand(new AddRole { Name = roleName })); + return PublishAsync(new AddRole { Name = roleName }); } private Task ExecuteAddLanguageAsync(Language language) { - return sut.ExecuteAsync(CreateCommand(new AddLanguage { Language = language })); + return PublishAsync(new AddLanguage { Language = language }); } private Task ExecuteAddWorkflowAsync() { - return sut.ExecuteAsync(CreateCommand(new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" })); + return PublishAsync(new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" }); } private Task ExecuteChangePlanAsync() { - return sut.ExecuteAsync(CreateCommand(new ChangePlan { PlanId = planIdPaid })); + return PublishAsync(new ChangePlan { PlanId = planIdPaid }); } private Task ExecuteArchiveAsync() { - return sut.ExecuteAsync(CreateCommand(new ArchiveApp())); + return PublishAsync(new ArchiveApp()); + } + + private async Task PublishIdempotentAsync(AppCommand command) + { + var result = await PublishAsync(command); + + var previousSnapshot = sut.Snapshot; + var previousVersion = sut.Snapshot.Version; + + await PublishAsync(command); + + Assert.Same(previousSnapshot, sut.Snapshot); + Assert.Equal(previousVersion, sut.Snapshot.Version); + + return result; + } + + private async Task PublishAsync(AppCommand command) + { + var result = await sut.ExecuteAsync(CreateCommand(command)); + + return result.Value; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs index f02f2b91c..56dc67242 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs @@ -70,14 +70,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } [Fact] - public async Task CanAssign_should_throw_exception_if_user_already_exists_with_same_role() + public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_same_role() { var command = new AssignContributor { ContributorId = "1", Role = Role.Owner }; var contributors_1 = contributors_0.Assign("1", Role.Owner); - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan), - new ValidationError("Contributor has already this role.", "Role")); + await GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs index a1e2d7605..ab2c38564 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs @@ -109,14 +109,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } [Fact] - public void CanChangePlan_should_throw_exception_if_plan_is_the_same() + public void CanChangePlan_should_not_throw_exception_if_plan_is_the_same() { var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; var plan = new AppPlan(command.Actor, "basic"); - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), - new ValidationError("App has already this plan.")); + GuardApp.CanChangePlan(command, plan, appPlans); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index acf5de44d..7c871b7b4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -16,7 +16,6 @@ using Orleans; using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; @@ -32,14 +31,12 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly IAssetEnricher assetEnricher = A.Fake(); private readonly IAssetFileStore assetFileStore = A.Fake(); private readonly IAssetQueryService assetQuery = A.Fake(); - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); + private readonly IAssetMetadataSource assetMetadataSource = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ITagGenerator tagGenerator = A.Fake>(); private readonly ITagService tagService = A.Fake(); private readonly Guid assetId = Guid.NewGuid(); private readonly Stream stream = new MemoryStream(); - private readonly ImageInfo image = new ImageInfo(2048, 2048); private readonly AssetGrain asset; private readonly AssetFile file; private readonly Context requestContext = Context.Anonymous(); @@ -73,15 +70,11 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => grainFactory.GetGrain(Id, null)) .Returns(asset); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) - .Returns(image); - sut = new AssetCommandMiddleware(grainFactory, assetEnricher, - assetQuery, assetFileStore, - assetThumbnailGenerator, - contextProvider, new[] { tagGenerator }); + assetQuery, + contextProvider, new[] { assetMetadataSource }); } [Fact] @@ -149,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Assets result.Asset.Should().BeEquivalentTo(asset.Snapshot, x => x.ExcludingMissingMembers()); AssertAssetHasBeenUploaded(0); - AssertAssetImageChecked(); + AssertMetadataEnriched(); } [Fact] @@ -234,7 +227,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.HandleAsync(context); AssertAssetHasBeenUploaded(1); - AssertAssetImageChecked(); + AssertMetadataEnriched(); } [Fact] @@ -307,9 +300,9 @@ namespace Squidex.Domain.Apps.Entities.Assets .Returns(new List { duplicate }); } - private void AssertAssetImageChecked() + private void AssertMetadataEnriched() { - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + A.CallTo(() => assetMetadataSource.EnhanceAsync(A.Ignored, A>.Ignored)) .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs index 0d44399d7..280bde17c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs @@ -64,9 +64,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAssetFolder { FolderName = "New Name" }; - var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(command.FolderName, sut.Snapshot.FolderName); @@ -86,18 +86,15 @@ namespace Squidex.Domain.Apps.Entities.Assets await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(command.FolderName, sut.Snapshot.FolderName); LastEvents .ShouldHaveSameEvents( - CreateAssetFolderEvent(new AssetFolderRenamed - { - FolderName = command.FolderName - }) + CreateAssetFolderEvent(new AssetFolderRenamed { FolderName = command.FolderName }) ); } @@ -108,9 +105,9 @@ namespace Squidex.Domain.Apps.Entities.Assets await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(parentId, sut.Snapshot.ParentId); @@ -127,9 +124,9 @@ namespace Squidex.Domain.Apps.Entities.Assets await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(1)); + result.ShouldBeEquivalent2(new EntitySavedResult(1)); Assert.True(sut.Snapshot.IsDeleted); @@ -141,17 +138,17 @@ namespace Squidex.Domain.Apps.Entities.Assets private Task ExecuteCreateAsync() { - return sut.ExecuteAsync(CreateAssetFolderCommand(new CreateAssetFolder { FolderName = "My Folder" })); + return PublishAsync(new CreateAssetFolder { FolderName = "My Folder" }); } private Task ExecuteUpdateAsync() { - return sut.ExecuteAsync(CreateAssetFolderCommand(new RenameAssetFolder { FolderName = "My Folder" })); + return PublishAsync(new RenameAssetFolder { FolderName = "My Folder" }); } private Task ExecuteDeleteAsync() { - return sut.ExecuteAsync(CreateAssetFolderCommand(new DeleteAssetFolder())); + return PublishAsync(new DeleteAssetFolder()); } protected T CreateAssetFolderEvent(T @event) where T : AssetFolderEvent @@ -167,5 +164,27 @@ namespace Squidex.Domain.Apps.Entities.Assets return CreateCommand(command); } + + private async Task PublishIdempotentAsync(AssetFolderCommand command) + { + var result = await PublishAsync(command); + + var previousSnapshot = sut.Snapshot; + var previousVersion = sut.Snapshot.Version; + + await PublishAsync(command); + + Assert.Same(previousSnapshot, sut.Snapshot); + Assert.Equal(previousVersion, sut.Snapshot.Version); + + return result; + } + + private async Task PublishAsync(AssetFolderCommand command) + { + var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); + + return result.Value; + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs index 23dc31f52..96fefcbdb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs @@ -8,8 +8,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using FakeItEasy; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.State; @@ -29,7 +31,6 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly ITagService tagService = A.Fake(); private readonly IAssetQueryService assetQuery = A.Fake(); private readonly IActivationLimit limit = A.Fake(); - private readonly ImageInfo image = new ImageInfo(2048, 2048); private readonly Guid parentId = Guid.NewGuid(); private readonly Guid assetId = Guid.NewGuid(); private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); @@ -46,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Assets .Returns(new List { A.Fake() }); A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A>.Ignored, A>.Ignored)) - .Returns(new Dictionary()); + .ReturnsLazily(x => Task.FromResult(x.GetArgument>(2)?.ToDictionary(x => x)!)); sut = new AssetGrain(Store, tagService, assetQuery, limit, A.Dummy()); sut.ActivateAsync(Id).Wait(); @@ -71,11 +72,11 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Create_should_create_events_and_update_state() { - var command = new CreateAsset { File = file, ImageInfo = image, FileHash = "NewHash", Tags = new HashSet() }; + var command = new CreateAsset { File = file, FileHash = "NewHash" }; - var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(0, sut.Snapshot.FileVersion); Assert.Equal(command.FileHash, sut.Snapshot.FileHash); @@ -84,14 +85,12 @@ namespace Squidex.Domain.Apps.Entities.Assets .ShouldHaveSameEvents( CreateAssetEvent(new AssetCreated { - IsImage = true, FileName = file.FileName, FileSize = file.FileSize, FileHash = command.FileHash, FileVersion = 0, + Metadata = new AssetMetadata(), MimeType = file.MimeType, - PixelWidth = image.PixelWidth, - PixelHeight = image.PixelHeight, Tags = new HashSet(), Slug = file.FileName.ToAssetSlug() }) @@ -101,13 +100,13 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Update_should_create_events_and_update_state() { - var command = new UpdateAsset { File = file, ImageInfo = image, FileHash = "NewHash" }; + var command = new UpdateAsset { File = file, FileHash = "NewHash" }; await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(1, sut.Snapshot.FileVersion); Assert.Equal(command.FileHash, sut.Snapshot.FileHash); @@ -116,13 +115,11 @@ namespace Squidex.Domain.Apps.Entities.Assets .ShouldHaveSameEvents( CreateAssetEvent(new AssetUpdated { - IsImage = true, FileSize = file.FileSize, FileHash = command.FileHash, FileVersion = 1, - MimeType = file.MimeType, - PixelWidth = image.PixelWidth, - PixelHeight = image.PixelHeight + Metadata = new AssetMetadata(), + MimeType = file.MimeType }) ); } @@ -134,15 +131,15 @@ namespace Squidex.Domain.Apps.Entities.Assets await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); - Assert.Equal("My New Image.png", sut.Snapshot.FileName); + Assert.Equal(command.FileName, sut.Snapshot.FileName); LastEvents .ShouldHaveSameEvents( - CreateAssetEvent(new AssetAnnotated { FileName = "My New Image.png" }) + CreateAssetEvent(new AssetAnnotated { FileName = command.FileName }) ); } @@ -153,32 +150,51 @@ namespace Squidex.Domain.Apps.Entities.Assets await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); - Assert.Equal("my-new-image.png", sut.Snapshot.Slug); + Assert.Equal(command.Slug, sut.Snapshot.Slug); LastEvents .ShouldHaveSameEvents( - CreateAssetEvent(new AssetAnnotated { Slug = "my-new-image.png" }) + CreateAssetEvent(new AssetAnnotated { Slug = command.Slug }) ); } [Fact] - public async Task AnnotateTag_should_create_events_and_update_state() + public async Task AnnotateMetadata_should_create_events_and_update_state() { - var command = new AnnotateAsset { Tags = new HashSet() }; + var command = new AnnotateAsset { Metadata = new AssetMetadata().SetPixelWidth(800) }; await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + var result = await PublishIdempotentAsync(command); + + result.ShouldBeEquivalent2(sut.Snapshot); - result.ShouldBeEquivalent(sut.Snapshot); + Assert.Equal(command.Metadata, sut.Snapshot.Metadata); LastEvents .ShouldHaveSameEvents( - CreateAssetEvent(new AssetAnnotated { Tags = new HashSet() }) + CreateAssetEvent(new AssetAnnotated { Metadata = command.Metadata }) + ); + } + + [Fact] + public async Task AnnotateTags_should_create_events_and_update_state() + { + var command = new AnnotateAsset { Tags = new HashSet { "tag1" } }; + + await ExecuteCreateAsync(); + + var result = await PublishIdempotentAsync(command); + + result.ShouldBeEquivalent2(sut.Snapshot); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetAnnotated { Tags = new HashSet { "tag1" } }) ); } @@ -189,9 +205,9 @@ namespace Squidex.Domain.Apps.Entities.Assets await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(parentId, sut.Snapshot.ParentId); @@ -209,9 +225,9 @@ namespace Squidex.Domain.Apps.Entities.Assets await ExecuteCreateAsync(); await ExecuteUpdateAsync(); - var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(2)); + result.ShouldBeEquivalent2(new EntitySavedResult(2)); Assert.True(sut.Snapshot.IsDeleted); @@ -223,17 +239,17 @@ namespace Squidex.Domain.Apps.Entities.Assets private Task ExecuteCreateAsync() { - return sut.ExecuteAsync(CreateAssetCommand(new CreateAsset { File = file })); + return PublishAsync(new CreateAsset { File = file }); } private Task ExecuteUpdateAsync() { - return sut.ExecuteAsync(CreateAssetCommand(new UpdateAsset { File = file })); + return PublishAsync(new UpdateAsset { File = file }); } private Task ExecuteDeleteAsync() { - return sut.ExecuteAsync(CreateAssetCommand(new DeleteAsset())); + return PublishAsync(new DeleteAsset()); } protected T CreateAssetEvent(T @event) where T : AssetEvent @@ -249,5 +265,27 @@ namespace Squidex.Domain.Apps.Entities.Assets return CreateCommand(command); } + + private async Task PublishIdempotentAsync(AssetCommand command) + { + var result = await PublishAsync(command); + + var previousSnapshot = sut.Snapshot; + var previousVersion = sut.Snapshot.Version; + + await PublishAsync(command); + + Assert.Same(previousSnapshot, sut.Snapshot); + Assert.Equal(previousVersion, sut.Snapshot.Version); + + return result; + } + + private async Task PublishAsync(AssetCommand command) + { + var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + + return result.Value; + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs index f07531700..6b9abd742 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure.Assets; using Xunit; @@ -19,39 +20,49 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly FileTypeTagGenerator sut = new FileTypeTagGenerator(); [Fact] - public void Should_not_add_tag_if_no_file_info() + public async Task Should_not_add_tag_if_no_file_info() { var command = new CreateAsset(); - sut.GenerateTags(command, tags); + await sut.EnhanceAsync(command, tags); Assert.Empty(tags); } [Fact] - public void Should_add_file_type() + public async Task Should_add_file_type() { var command = new CreateAsset { File = new AssetFile("File.DOCX", "Mime", 100, () => new MemoryStream()) }; - sut.GenerateTags(command, tags); + await sut.EnhanceAsync(command, tags); Assert.Contains("type/docx", tags); } [Fact] - public void Should_add_blob_if_without_extension() + public async Task Should_add_blob_if_without_extension() { var command = new CreateAsset { File = new AssetFile("File", "Mime", 100, () => new MemoryStream()) }; - sut.GenerateTags(command, tags); + await sut.EnhanceAsync(command, tags); Assert.Contains("type/blob", tags); } + + [Fact] + public void Should_always_format_to_empty() + { + var source = new AssetEntity(); + + var formatted = sut.Format(source); + + Assert.Empty(formatted); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetFolderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetFolderTests.cs index 958446d3c..99edf6234 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetFolderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetFolderTests.cs @@ -94,23 +94,22 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards } [Fact] - public async Task CanMove_should_throw_exception_when_folder_has_not_changed() + public async Task CanMove_should_not_throw_exception_when_folder_found() { var command = new MoveAssetFolder { ParentId = Guid.NewGuid() }; - await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), command.ParentId), - new ValidationError("Asset folder is already part of this folder.", "ParentId")); + A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) + .Returns(new List { CreateFolder() }); + + await GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), Guid.NewGuid()); } [Fact] - public async Task CanMove_should_not_throw_exception_when_folder_found() + public async Task CanMove_should_not_throw_exception_when_folder_has_not_changed() { var command = new MoveAssetFolder { ParentId = Guid.NewGuid() }; - A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) - .Returns(new List { CreateFolder() }); - - await GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), Guid.NewGuid()); + await GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), command.ParentId); } [Fact] @@ -126,25 +125,16 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards { var command = new RenameAssetFolder(); - ValidationAssert.Throws(() => GuardAssetFolder.CanRename(command, "My Folder"), + ValidationAssert.Throws(() => GuardAssetFolder.CanRename(command), new ValidationError("Folder name is required.", "FolderName")); } - [Fact] - public void CanRename_should_throw_exception_if_names_are_the_same() - { - var command = new RenameAssetFolder { FolderName = "My Folder" }; - - ValidationAssert.Throws(() => GuardAssetFolder.CanRename(command, "My Folder"), - new ValidationError("Asset folder has already this name.", "FolderName")); - } - [Fact] public void CanRename_should_not_throw_exception_if_names_are_different() { var command = new RenameAssetFolder { FolderName = "New Folder Name" }; - GuardAssetFolder.CanRename(command, "My Folder"); + GuardAssetFolder.CanRename(command); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs index b20646831..477b88d94 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs @@ -64,12 +64,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards } [Fact] - public async Task CanMove_should_throw_exception_when_folder_has_not_changed() + public async Task CanMove_should_not_throw_exception_when_folder_has_not_changed() { var command = new MoveAsset { ParentId = Guid.NewGuid() }; - await ValidationAssert.ThrowsAsync(() => GuardAsset.CanMove(command, assetQuery, command.ParentId), - new ValidationError("Asset is already part of this folder.", "ParentId")); + await GuardAsset.CanMove(command, assetQuery, command.ParentId); } [Fact] @@ -96,8 +95,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards { var command = new AnnotateAsset(); - ValidationAssert.Throws(() => GuardAsset.CanAnnotate(command, "asset-name", "asset-slug"), - new ValidationError("Either file name, slug or tags must be defined.", "FileName", "Slug", "Tags")); + ValidationAssert.Throws(() => GuardAsset.CanAnnotate(command), + new ValidationError("Either file name, slug, tags or metadata must be defined.", "FileName", "Slug", "Tags", "Metadata")); } [Fact] @@ -105,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards { var command = new AnnotateAsset { FileName = "new-name", Slug = "new-slug" }; - GuardAsset.CanAnnotate(command, "asset-name", "asset-slug"); + GuardAsset.CanAnnotate(command); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageMetadataSourceTests.cs new file mode 100644 index 000000000..61eea86a9 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageMetadataSourceTests.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure.Assets; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class ImageMetadataSourceTests + { + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); + private readonly HashSet tags = new HashSet(); + private readonly MemoryStream stream = new MemoryStream(); + private readonly AssetFile file; + private readonly ImageMetadataSource sut; + + public ImageMetadataSourceTests() + { + file = new AssetFile("MyImage.png", "image/png", 1024, () => stream); + + sut = new ImageMetadataSource(assetThumbnailGenerator); + } + + [Fact] + public async Task Should_not_add_tag_if_no_image() + { + var command = new CreateAsset { File = file }; + + await sut.EnhanceAsync(command, tags); + + Assert.Empty(tags); + } + + [Fact] + public async Task Should_add_image_tag_if_small() + { + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(new ImageInfo(100, 100)); + + var command = new CreateAsset { File = file }; + + await sut.EnhanceAsync(command, tags); + + Assert.Contains("image", tags); + Assert.Contains("image/small", tags); + } + + [Fact] + public async Task Should_add_image_tag_if_medium() + { + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(new ImageInfo(800, 600)); + + var command = new CreateAsset { File = file }; + + await sut.EnhanceAsync(command, tags); + + Assert.Contains("image", tags); + Assert.Contains("image/medium", tags); + } + + [Fact] + public async Task Should_add_image_tag_if_large() + { + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(new ImageInfo(1200, 1400)); + + var command = new CreateAsset { File = file }; + + await sut.EnhanceAsync(command, tags); + + Assert.Contains("image", tags); + Assert.Contains("image/large", tags); + } + + [Fact] + public void Should_format_with_dimensions_if_image() + { + var source = new AssetEntity + { + Metadata = + new AssetMetadata() + .SetPixelWidth(800) + .SetPixelHeight(600), + Type = AssetType.Image + }; + + var formatted = sut.Format(source).First(); + + Assert.Equal("800x600px", formatted); + } + + [Fact] + public void Should_format_to_empty_if_not_an_image() + { + var source = new AssetEntity(); + + var formatted = sut.Format(source); + + Assert.Empty(formatted); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs deleted file mode 100644 index 2600bcbbb..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs +++ /dev/null @@ -1,72 +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.Infrastructure.Assets; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public class ImageTagGeneratorTests - { - private readonly HashSet tags = new HashSet(); - private readonly ImageTagGenerator sut = new ImageTagGenerator(); - - [Fact] - public void Should_not_add_tag_if_no_image() - { - var command = new CreateAsset(); - - sut.GenerateTags(command, tags); - - Assert.Empty(tags); - } - - [Fact] - public void Should_add_image_tag_if_small() - { - var command = new CreateAsset - { - ImageInfo = new ImageInfo(100, 100) - }; - - sut.GenerateTags(command, tags); - - Assert.Contains("image", tags); - Assert.Contains("image/small", tags); - } - - [Fact] - public void Should_add_image_tag_if_medium() - { - var command = new CreateAsset - { - ImageInfo = new ImageInfo(800, 600) - }; - - sut.GenerateTags(command, tags); - - Assert.Contains("image", tags); - Assert.Contains("image/medium", tags); - } - - [Fact] - public void Should_add_image_tag_if_large() - { - var command = new CreateAsset - { - ImageInfo = new ImageInfo(1200, 1400) - }; - - sut.GenerateTags(command, tags); - - Assert.Contains("image", tags); - Assert.Contains("image/large", tags); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs index a5d5c3c77..5181d9312 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs @@ -110,15 +110,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb Assert.Equal(o, i); } - [Fact] - public void Should_make_query_with_isImage() - { - var i = F(ClrFilter.Eq("isImage", true)); - var o = C("{ 'im' : true }"); - - Assert.Equal(o, i); - } - [Fact] public void Should_make_query_with_mimeType() { @@ -140,8 +131,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb [Fact] public void Should_make_query_with_pixelHeight() { - var i = F(ClrFilter.Eq("pixelHeight", 600)); - var o = C("{ 'ph' : 600 }"); + var i = F(ClrFilter.Eq("metadata.pixelHeight", 600)); + var o = C("{ 'md.pixelHeight' : 600 }"); Assert.Equal(o, i); } @@ -149,8 +140,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb [Fact] public void Should_make_query_with_pixelWidth() { - var i = F(ClrFilter.Eq("pixelWidth", 800)); - var o = C("{ 'pw' : 800 }"); + var i = F(ClrFilter.Eq("metadata.pixelWidth", 800)); + var o = C("{ 'md.pixelWidth' : 800 }"); Assert.Equal(o, i); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs index 7945cfe47..09a203b9e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Xunit; @@ -18,13 +19,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries public class AssetEnricherTests { private readonly ITagService tagService = A.Fake(); + private readonly IAssetMetadataSource assetMetadataSource1 = A.Fake(); + private readonly IAssetMetadataSource assetMetadataSource2 = A.Fake(); private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly Context requestContext = Context.Anonymous(); private readonly AssetEnricher sut; public AssetEnricherTests() { - sut = new AssetEnricher(tagService); + var assetMetadataSources = new[] + { + assetMetadataSource1, + assetMetadataSource2 + }; + + sut = new AssetEnricher(tagService, assetMetadataSources); } [Fact] @@ -50,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AppId = appId }; - A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A>.That.IsSameSequenceAs("id1", "id2"))) + A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A>.That.Has("id1", "id2"))) .Returns(new Dictionary { ["id1"] = "name1", @@ -80,6 +89,31 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries Assert.Null(result.TagNames); } + [Fact] + public async Task Should_enrich_asset_with_metadata() + { + var source = new AssetEntity + { + FileSize = 2 * 1024, + Tags = new HashSet + { + "id1", + "id2" + }, + AppId = appId + }; + + A.CallTo(() => assetMetadataSource1.Format(A.Ignored)) + .Returns(new[] { "metadata1" }); + + A.CallTo(() => assetMetadataSource2.Format(A.Ignored)) + .Returns(new[] { "metadata2", "metadata3" }); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Equal("metadata1, metadata2, metadata3, 2 kB", result.MetadataText); + } + [Fact] public async Task Should_enrich_multiple_assets_with_tag_names() { @@ -103,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AppId = appId }; - A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A>.That.IsSameSequenceAs("id1", "id2", "id3"))) + A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A>.That.Has("id1", "id2", "id3"))) .Returns(new Dictionary { ["id1"] = "name1", diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 73efc9fc5..17e014fdf 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -148,6 +148,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL isImage pixelWidth pixelHeight + type + metadataText + metadataPixelWidth: metadata(path: ""pixelWidth"") + metadataUnknown: metadata(path: ""unknown"") + metadata tags slug } @@ -185,7 +190,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL isImage = true, pixelWidth = 800, pixelHeight = 600, - tags = new[] { "tag1", "tag2" }, + type = "IMAGE", + metadataText = "metadata-text", + metadataPixelWidth = 800, + metadataUnknown = (string?)null, + metadata = new + { + pixelWidth = 800, + pixelHeight = 600, + }, + tags = new[] + { + "tag1", + "tag2" + }, slug = "myfile.png" } } @@ -220,6 +238,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL isImage pixelWidth pixelHeight + type + metadataText + metadataPixelWidth: metadata(path: ""pixelWidth"") + metadataUnknown: metadata(path: ""unknown"") + metadata tags slug } @@ -261,7 +284,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL isImage = true, pixelWidth = 800, pixelHeight = 600, - tags = new[] { "tag1", "tag2" }, + type = "IMAGE", + metadataText = "metadata-text", + metadataPixelWidth = 800, + metadataUnknown = (string?)null, + metadata = new + { + pixelWidth = 800, + pixelHeight = 600, + }, + tags = new[] + { + "tag1", + "tag2" + }, slug = "myfile.png" } } @@ -388,6 +424,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL myNumber myBoolean myDatetime + myJsonValue: myJson(path: ""value"") myJson myGeolocation myTags @@ -430,6 +467,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL myNumber = 1, myBoolean = true, myDatetime = content.LastModified, + myJsonValue = 1, myJson = new { value = 1 diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index c188160f9..21fb0a312 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; using NodaTime; using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Apps; @@ -228,9 +229,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL FileHash = "ABC123", FileVersion = 123, MimeType = "image/png", - IsImage = true, - PixelWidth = 800, - PixelHeight = 600, + Type = AssetType.Image, + MetadataText = "metadata-text", + Metadata = + new AssetMetadata() + .SetPixelWidth(800) + .SetPixelHeight(600), TagNames = new[] { "tag1", "tag2" }.ToHashSet() }; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs index 7333491dc..fb57307e2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.Schemas; @@ -71,11 +72,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_add_assets_id_and_versions_as_dependency() { - var image1 = CreateAsset(Guid.NewGuid(), 1, true); - var image2 = CreateAsset(Guid.NewGuid(), 2, true); + var image1 = CreateAsset(Guid.NewGuid(), 1, AssetType.Image); + var image2 = CreateAsset(Guid.NewGuid(), 2, AssetType.Image); - var document1 = CreateAsset(Guid.NewGuid(), 3, false); - var document2 = CreateAsset(Guid.NewGuid(), 4, false); + var document1 = CreateAsset(Guid.NewGuid(), 3, AssetType.Unknown); + var document2 = CreateAsset(Guid.NewGuid(), 4, AssetType.Unknown); var source = new IContentEntity[] { @@ -106,11 +107,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_enrich_with_asset_urls() { - var image1 = CreateAsset(Guid.NewGuid(), 1, true); - var image2 = CreateAsset(Guid.NewGuid(), 2, true); + var image1 = CreateAsset(Guid.NewGuid(), 1, AssetType.Image); + var image2 = CreateAsset(Guid.NewGuid(), 2, AssetType.Image); - var document1 = CreateAsset(Guid.NewGuid(), 3, false); - var document2 = CreateAsset(Guid.NewGuid(), 4, false); + var document1 = CreateAsset(Guid.NewGuid(), 3, AssetType.Unknown); + var document2 = CreateAsset(Guid.NewGuid(), 4, AssetType.Unknown); var source = new IContentEntity[] { @@ -223,9 +224,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries }; } - private static IEnrichedAssetEntity CreateAsset(Guid id, int version, bool isImage) + private static IEnrichedAssetEntity CreateAsset(Guid id, int version, AssetType type) { - return new AssetEntity { Id = id, IsImage = isImage, Version = version }; + return new AssetEntity { Id = id, Type = type, Version = version }; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerBenchmark.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerBenchmark.cs index 5dc9f60d0..be4f36380 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerBenchmark.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerBenchmark.cs @@ -13,6 +13,8 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Log; using Xunit; +#pragma warning disable xUnit1004 // Test methods should not be skipped + namespace Squidex.Domain.Apps.Entities.Contents.Text { public class TextIndexerBenchmark diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs index a5df505a9..7273c1468 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs @@ -24,7 +24,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards public class GuardRuleTests { private readonly Uri validUrl = new Uri("https://squidex.io"); - private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction()).Rename("MyName"); private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); private readonly IAppProvider appProvider = A.Fake(); @@ -95,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards { var command = new UpdateRule(); - await ValidationAssert.ThrowsAsync(() => GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0), + await ValidationAssert.ThrowsAsync(() => GuardRule.CanUpdate(command, appId.Id, appProvider), new ValidationError("Either trigger, action or name is required.", "Trigger", "Action")); } @@ -104,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards { var command = new UpdateRule { Name = "MyName" }; - await GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0); + await GuardRule.CanUpdate(command, appId.Id, appProvider); } [Fact] @@ -123,47 +122,23 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards Name = "NewName" }; - await GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0); + await GuardRule.CanUpdate(command, appId.Id, appProvider); } [Fact] - public void CanEnable_should_throw_exception_if_rule_enabled() + public void CanEnable_should_not_throw_exception() { var command = new EnableRule(); - var rule_1 = rule_0.Enable(); - - Assert.Throws(() => GuardRule.CanEnable(command, rule_1)); - } - - [Fact] - public void CanEnable_should_not_throw_exception_if_rule_disabled() - { - var command = new EnableRule(); - - var rule_1 = rule_0.Disable(); - - GuardRule.CanEnable(command, rule_1); + GuardRule.CanEnable(command); } [Fact] - public void CanDisable_should_throw_exception_if_rule_disabled() + public void CanDisable_should_not_throw_exception() { var command = new DisableRule(); - var rule_1 = rule_0.Disable(); - - Assert.Throws(() => GuardRule.CanDisable(command, rule_1)); - } - - [Fact] - public void CanDisable_should_not_throw_exception_if_rule_enabled() - { - var command = new DisableRule(); - - var rule_1 = rule_0.Enable(); - - GuardRule.CanDisable(command, rule_1); + GuardRule.CanDisable(command); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs index 9c8096d98..01f3837ca 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs @@ -59,9 +59,9 @@ namespace Squidex.Domain.Apps.Entities.Rules { var command = MakeCreateCommand(); - var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(AppId, sut.Snapshot.AppId.Id); @@ -81,9 +81,9 @@ namespace Squidex.Domain.Apps.Entities.Rules await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(1)); + result.ShouldBeEquivalent2(new EntitySavedResult(1)); Assert.Same(command.Trigger, sut.Snapshot.RuleDef.Trigger); Assert.Same(command.Action, sut.Snapshot.RuleDef.Action); @@ -96,13 +96,6 @@ namespace Squidex.Domain.Apps.Entities.Rules ); } - [Fact] - public async Task Enable_should_handle_command() - { - await sut.ExecuteAsync(CreateRuleCommand(MakeCreateCommand())); - await sut.ExecuteAsync(CreateRuleCommand(new DisableRule())); - } - [Fact] public async Task Enable_should_create_events_and_update_state() { @@ -111,9 +104,9 @@ namespace Squidex.Domain.Apps.Entities.Rules await ExecuteCreateAsync(); await ExecuteDisableAsync(); - var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(sut.Snapshot.RuleDef.IsEnabled); @@ -130,9 +123,9 @@ namespace Squidex.Domain.Apps.Entities.Rules await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.False(sut.Snapshot.RuleDef.IsEnabled); @@ -149,9 +142,9 @@ namespace Squidex.Domain.Apps.Entities.Rules await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(1)); + result.ShouldBeEquivalent2(new EntitySavedResult(1)); Assert.True(sut.Snapshot.IsDeleted); @@ -168,9 +161,9 @@ namespace Squidex.Domain.Apps.Entities.Rules await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + var result = await PublishAsync(command); - Assert.Null(result.Value); + Assert.Null(result); A.CallTo(() => ruleEnqueuer.Enqueue(sut.Snapshot.RuleDef, sut.Id, A>.That.Matches(x => x.Payload is RuleManuallyTriggered))) @@ -179,17 +172,17 @@ namespace Squidex.Domain.Apps.Entities.Rules private Task ExecuteCreateAsync() { - return sut.ExecuteAsync(CreateRuleCommand(MakeCreateCommand())); + return PublishAsync(MakeCreateCommand()); } private Task ExecuteDisableAsync() { - return sut.ExecuteAsync(CreateRuleCommand(new DisableRule())); + return PublishAsync(new DisableRule()); } private Task ExecuteDeleteAsync() { - return sut.ExecuteAsync(CreateRuleCommand(new DeleteRule())); + return PublishAsync(new DeleteRule()); } protected T CreateRuleEvent(T @event) where T : RuleEvent @@ -208,7 +201,7 @@ namespace Squidex.Domain.Apps.Entities.Rules private static CreateRule MakeCreateCommand() { - var newTrigger = new ManualTrigger(); + var newTrigger = new ContentChangedTriggerV2(); var newAction = new TestAction { Value = 123 }; return new CreateRule { Trigger = newTrigger, Action = newAction }; @@ -216,10 +209,32 @@ namespace Squidex.Domain.Apps.Entities.Rules private static UpdateRule MakeUpdateCommand() { - var newTrigger = new ManualTrigger(); - var newAction = new TestAction { Value = 123 }; + var newTrigger = new ContentChangedTriggerV2 { HandleAll = true }; + var newAction = new TestAction { Value = 456 }; return new UpdateRule { Trigger = newTrigger, Action = newAction, Name = "NewName" }; } + + private async Task PublishIdempotentAsync(RuleCommand command) + { + var result = await PublishAsync(command); + + var previousSnapshot = sut.Snapshot; + var previousVersion = sut.Snapshot.Version; + + await PublishAsync(command); + + Assert.Same(previousSnapshot, sut.Snapshot); + Assert.Equal(previousVersion, sut.Snapshot.Version); + + return result; + } + + private async Task PublishAsync(RuleCommand command) + { + var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + + return result.Value; + } } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs index d79c61ba8..2f93e586d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs @@ -37,12 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards .AddUI(4, "field4", Partitioning.Invariant); } - private static Action A(Action method) where T : FieldCommand - { - return method; - } - - private static Func S(Func method) + private static Action A(Action method) where T : FieldCommand { return method; } @@ -58,137 +53,97 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards new object[] { A(GuardSchemaField.CanUpdate) } }; - public static IEnumerable InvalidStates = new[] - { - new object[] { A(GuardSchemaField.CanDisable), S(s => s.DisableField(1)) }, - new object[] { A(GuardSchemaField.CanEnable), S(s => s) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s.HideField(1)) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s.LockField(1)) }, - new object[] { A(GuardSchemaField.CanLock), S(s => s.LockField(1)) } - }; - - public static IEnumerable InvalidNestedStates = new[] - { - new object[] { A(GuardSchemaField.CanDisable), S(s => s.DisableField(301, 3)) }, - new object[] { A(GuardSchemaField.CanEnable), S(s => s) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s.HideField(301, 3)) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s) }, - new object[] { A(GuardSchemaField.CanLock), S(s => s.LockField(301, 3)) } - }; - - public static IEnumerable ValidStates = new[] - { - new object[] { A(GuardSchemaField.CanDisable), S(s => s) }, - new object[] { A(GuardSchemaField.CanEnable), S(s => s.DisableField(1)) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s.HideField(1)) } - }; - - public static IEnumerable ValidNestedStates = new[] - { - new object[] { A(GuardSchemaField.CanEnable), S(s => s.DisableField(301, 3)) }, - new object[] { A(GuardSchemaField.CanDisable), S(s => s) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s.HideField(301, 3)) } - }; - [Theory] [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_field_not_found(Action action) where T : FieldCommand, new() + public void Commands_should_throw_exception_if_field_not_found(Action action) where T : FieldCommand, new() { var command = new T { FieldId = 5 }; - Assert.Throws(() => action(schema_0, command)); + Assert.Throws(() => action(command, schema_0)); } [Theory] [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_parent_field_not_found(Action action) where T : FieldCommand, new() + public void Commands_should_throw_exception_if_parent_field_not_found(Action action) where T : FieldCommand, new() { var command = new T { ParentFieldId = 4, FieldId = 401 }; - Assert.Throws(() => action(schema_0, command)); + Assert.Throws(() => action(command, schema_0)); } [Theory] [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_child_field_not_found(Action action) where T : FieldCommand, new() + public void Commands_should_throw_exception_if_child_field_not_found(Action action) where T : FieldCommand, new() { var command = new T { ParentFieldId = 3, FieldId = 302 }; - Assert.Throws(() => action(schema_0, command)); + Assert.Throws(() => action(command, schema_0)); } - [Theory] - [MemberData(nameof(InvalidStates))] - public void Commands_should_throw_exception_if_state_not_valid(Action action, Func updater) where T : FieldCommand, new() + [Fact] + public void CanDisable_should_not_throw_exception_if_already_disabled() { - var command = new T { FieldId = 1 }; - - Assert.Throws(() => action(updater(schema_0), command)); - } + var command = new DisableField { FieldId = 1 }; - [Theory] - [MemberData(nameof(InvalidNestedStates))] - public void Commands_should_throw_exception_if_nested_state_not_valid(Action action, Func updater) where T : FieldCommand, new() - { - var command = new T { ParentFieldId = 3, FieldId = 301 }; + var schema_1 = schema_0.UpdateField(1, f => f.Disable()); - Assert.Throws(() => action(updater(schema_0), command)); + GuardSchemaField.CanDisable(command, schema_1); } - [Theory] - [MemberData(nameof(ValidStates))] - public void Commands_should_not_throw_exception_if_state_valid(Action action, Func updater) where T : FieldCommand, new() + [Fact] + public void CanDisable_should_throw_exception_if_locked() { - var command = new T { FieldId = 1 }; + var command = new DisableField { FieldId = 1 }; + + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - action(updater(schema_0), command); + Assert.Throws(() => GuardSchemaField.CanDisable(command, schema_1)); } - [Theory] - [MemberData(nameof(ValidNestedStates))] - public void Commands_should_not_throw_exception_if_nested_state_valid(Action action, Func updater) where T : FieldCommand, new() + [Fact] + public void CanDisable_should_throw_exception_if_ui_field() { - var command = new T { ParentFieldId = 3, FieldId = 301 }; + var command = new DisableField { FieldId = 4 }; - action(updater(schema_0), command); + Assert.Throws(() => GuardSchemaField.CanDisable(command, schema_0)); } [Fact] - public void CanDelete_should_throw_exception_if_locked() + public void CanEnable_should_not_throw_exception_if_already_enabled() { - var command = new DeleteField { FieldId = 1 }; + var command = new EnableField { FieldId = 1 }; - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + var schema_1 = schema_0.UpdateField(1, f => f.Enable()); - Assert.Throws(() => GuardSchemaField.CanDelete(schema_1, command)); + GuardSchemaField.CanEnable(command, schema_1); } [Fact] - public void CanDisable_should_throw_exception_if_already_disabled() + public void CanEnable_should_throw_exception_if_locked() { - var command = new DisableField { FieldId = 1 }; + var command = new EnableField { FieldId = 1 }; - var schema_1 = schema_0.UpdateField(1, f => f.Disable()); + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - Assert.Throws(() => GuardSchemaField.CanDisable(schema_1, command)); + Assert.Throws(() => GuardSchemaField.CanEnable(command, schema_1)); } [Fact] - public void CanDisable_should_throw_exception_if_ui_field() + public void CanEnable_should_throw_exception_if_ui_field() { - var command = new DisableField { FieldId = 4 }; + var command = new EnableField { FieldId = 4 }; - Assert.Throws(() => GuardSchemaField.CanDisable(schema_0, command)); + Assert.Throws(() => GuardSchemaField.CanEnable(command, schema_0)); } [Fact] - public void CanEnable_should_throw_exception_if_already_enabled() + public void CanHide_should_not_throw_exception_if_already_hidden() { var command = new EnableField { FieldId = 1 }; - Assert.Throws(() => GuardSchemaField.CanEnable(schema_0, command)); + var schema_1 = schema_0.UpdateField(1, f => f.Hide()); + + GuardSchemaField.CanEnable(command, schema_1); } [Fact] @@ -198,33 +153,53 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); + Assert.Throws(() => GuardSchemaField.CanHide(command, schema_1)); } [Fact] - public void CanHide_should_throw_exception_if_already_hidden() + public void CanHide_should_throw_exception_if_ui_field() { - var command = new HideField { FieldId = 1 }; + var command = new HideField { FieldId = 4 }; - var schema_1 = schema_0.UpdateField(1, f => f.Hide()); + Assert.Throws(() => GuardSchemaField.CanHide(command, schema_0)); + } + + [Fact] + public void CanShow_should_not_throw_exception_if_already_shown() + { + var command = new EnableField { FieldId = 1 }; - Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); + var schema_1 = schema_0.UpdateField(1, f => f.Show()); + + GuardSchemaField.CanEnable(command, schema_1); } [Fact] - public void CanHide_should_throw_exception_if_ui_field() + public void CanShow_should_throw_exception_if_locked() { - var command = new HideField { FieldId = 4 }; + var command = new ShowField { FieldId = 1 }; + + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - Assert.Throws(() => GuardSchemaField.CanHide(schema_0, command)); + Assert.Throws(() => GuardSchemaField.CanShow(command, schema_1)); } [Fact] - public void CanShow_should_throw_exception_if_already_visible() + public void CanShow_should_throw_exception_if_ui_field() { var command = new ShowField { FieldId = 4 }; - Assert.Throws(() => GuardSchemaField.CanShow(schema_0, command)); + Assert.Throws(() => GuardSchemaField.CanShow(command, schema_0)); + } + + [Fact] + public void CanDelete_should_throw_exception_if_locked() + { + var command = new DeleteField { FieldId = 1 }; + + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + + Assert.Throws(() => GuardSchemaField.CanDelete(command, schema_1)); } [Fact] @@ -232,7 +207,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new DeleteField { FieldId = 1 }; - GuardSchemaField.CanDelete(schema_0, command); + GuardSchemaField.CanDelete(command, schema_0); } [Fact] @@ -242,7 +217,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - Assert.Throws(() => GuardSchemaField.CanUpdate(schema_1, command)); + Assert.Throws(() => GuardSchemaField.CanUpdate(command, schema_1)); } [Fact] @@ -250,7 +225,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new UpdateField { FieldId = 1, Properties = validProperties }; - GuardSchemaField.CanUpdate(schema_0, command); + GuardSchemaField.CanUpdate(command, schema_0); } [Fact] @@ -258,7 +233,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new UpdateField { FieldId = 2, Properties = null! }; - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(command, schema_0), new ValidationError("Properties is required.", "Properties")); } @@ -267,7 +242,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new UpdateField { FieldId = 2, Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } }; - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(command, schema_0), new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); } @@ -276,7 +251,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new AddField { Name = "field1", Properties = validProperties }; - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), new ValidationError("A field with the same name already exists.")); } @@ -285,7 +260,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new AddField { Name = "field301", Properties = validProperties, ParentFieldId = 3 }; - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), new ValidationError("A field with the same name already exists.")); } @@ -294,7 +269,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new AddField { Name = "INVALID_NAME", Properties = validProperties }; - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), new ValidationError("Name is not a Javascript property name.", "Name")); } @@ -303,7 +278,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new AddField { Name = "field5", Properties = invalidProperties }; - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); } @@ -312,7 +287,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new AddField { Name = "field5", Properties = null! }; - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), new ValidationError("Properties is required.", "Properties")); } @@ -321,7 +296,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new AddField { Name = "field5", Partitioning = "INVALID_PARTITIONING", Properties = validProperties }; - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), new ValidationError("Partitioning is not a valid value.", "Partitioning")); } @@ -330,7 +305,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new AddField { Name = "field302", Properties = validProperties, ParentFieldId = 99 }; - Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); + Assert.Throws(() => GuardSchemaField.CanAdd(command, schema_0)); } [Fact] @@ -338,7 +313,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new AddField { Name = "field5", Properties = validProperties }; - GuardSchemaField.CanAdd(schema_0, command); + GuardSchemaField.CanAdd(command, schema_0); } [Fact] @@ -346,7 +321,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new AddField { Name = "field1", Properties = validProperties, ParentFieldId = 3 }; - GuardSchemaField.CanAdd(schema_0, command); + GuardSchemaField.CanAdd(command, schema_0); } } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs index f2ca65049..1b077b24f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs @@ -508,7 +508,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards FieldsInReferences = null }; - ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(schema_0, command), + ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(command, schema_0), new ValidationError("Field is required.", "FieldsInLists[1]"), new ValidationError("Field is required.", @@ -530,7 +530,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards FieldsInReferences = new FieldNames(null!, null!, "field3", "field1", "field1", "field4") }; - ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(schema_0, command), + ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(command, schema_0), new ValidationError("Field is required.", "FieldsInReferences[1]"), new ValidationError("Field is required.", @@ -552,7 +552,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards FieldsInReferences = new FieldNames("meta.id") }; - ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(schema_0, command), + ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(command, schema_0), new ValidationError("Field is not part of the schema.", "FieldsInReferences[1]")); } @@ -566,43 +566,23 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards FieldsInReferences = new FieldNames("field2") }; - GuardSchema.CanConfigureUIFields(schema_0, command); + GuardSchema.CanConfigureUIFields(command, schema_0); } [Fact] - public void CanPublish_should_throw_exception_if_already_published() + public void CanPublish_should_not_throw_exception() { var command = new PublishSchema(); - var schema_1 = schema_0.Publish(); - - Assert.Throws(() => GuardSchema.CanPublish(schema_1, command)); - } - - [Fact] - public void CanPublish_should_not_throw_exception_if_not_published() - { - var command = new PublishSchema(); - - GuardSchema.CanPublish(schema_0, command); + GuardSchema.CanPublish(command); } [Fact] - public void CanUnpublish_should_throw_exception_if_already_unpublished() + public void CanUnpublish_should_not_throw_exception() { var command = new UnpublishSchema(); - Assert.Throws(() => GuardSchema.CanUnpublish(schema_0, command)); - } - - [Fact] - public void CanUnpublish_should_not_throw_exception_if_already_published() - { - var command = new UnpublishSchema(); - - var schema_1 = schema_0.Publish(); - - GuardSchema.CanUnpublish(schema_1, command); + GuardSchema.CanUnpublish(command); } [Fact] @@ -610,7 +590,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new ReorderFields { FieldIds = new List { 1, 3 } }; - ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), + ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), new ValidationError("Field ids do not cover all fields.", "FieldIds")); } @@ -619,7 +599,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new ReorderFields { FieldIds = new List { 1 } }; - ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), + ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), new ValidationError("Field ids do not cover all fields.", "FieldIds")); } @@ -628,7 +608,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new ReorderFields { FieldIds = null! }; - ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), + ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), new ValidationError("Field ids is required.", "FieldIds")); } @@ -637,7 +617,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new ReorderFields { FieldIds = new List { 1, 2 }, ParentFieldId = 99 }; - Assert.Throws(() => GuardSchema.CanReorder(schema_0, command)); + Assert.Throws(() => GuardSchema.CanReorder(command, schema_0)); } [Fact] @@ -645,7 +625,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new ReorderFields { FieldIds = new List { 1, 2, 4 } }; - GuardSchema.CanReorder(schema_0, command); + GuardSchema.CanReorder(command, schema_0); } [Fact] @@ -670,7 +650,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new ChangeCategory(); - GuardSchema.CanChangeCategory(schema_0, command); + GuardSchema.CanChangeCategory(command); } [Fact] @@ -678,7 +658,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new DeleteSchema(); - GuardSchema.CanDelete(schema_0, command); + GuardSchema.CanDelete(command); } private static StringFieldProperties ValidProperties() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs index 8bc8fecbf..8d32952a6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs @@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas public SchemaGrainTests() { - sut = new SchemaGrain(Store, A.Dummy(), TestUtils.DefaultSerializer); + sut = new SchemaGrain(Store, A.Dummy()); sut.ActivateAsync(Id).Wait(); } @@ -59,9 +59,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties, IsSingleton = true }; - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(AppId, sut.Snapshot.AppId.Id); @@ -99,9 +99,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties, Fields = fields }; - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); var @event = (SchemaCreated)LastEvents.Single().Payload; @@ -115,13 +115,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas [Fact] public async Task Update_should_create_events_and_update_state() { - var command = new UpdateSchema { Properties = new SchemaProperties() }; + var command = new UpdateSchema { Properties = new SchemaProperties { Label = "My Properties" } }; await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.Properties); @@ -144,9 +144,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -165,9 +165,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(command.FieldsInLists, sut.Snapshot.SchemaDef.FieldsInLists); @@ -188,9 +188,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(command.FieldsInReferences, sut.Snapshot.SchemaDef.FieldsInReferences); @@ -207,9 +207,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(sut.Snapshot.SchemaDef.IsPublished); @@ -227,9 +227,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); await ExecutePublishAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.False(sut.Snapshot.SchemaDef.IsPublished); @@ -246,9 +246,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(command.Name, sut.Snapshot.SchemaDef.Category); @@ -271,9 +271,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(command.PreviewUrls, sut.Snapshot.SchemaDef.PreviewUrls); @@ -290,9 +290,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(1)); + result.ShouldBeEquivalent2(new EntitySavedResult(1)); Assert.True(sut.Snapshot.IsDeleted); @@ -305,15 +305,15 @@ namespace Squidex.Domain.Apps.Entities.Schemas [Fact] public async Task Reorder_should_create_events_and_update_state() { - var command = new ReorderFields { FieldIds = new List { 1, 2 } }; + var command = new ReorderFields { FieldIds = new List { 2, 1 } }; await ExecuteCreateAsync(); await ExecuteAddFieldAsync("field1"); await ExecuteAddFieldAsync("field2"); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -324,16 +324,16 @@ namespace Squidex.Domain.Apps.Entities.Schemas [Fact] public async Task Reorder_should_create_events_and_update_state_for_array() { - var command = new ReorderFields { ParentFieldId = 1, FieldIds = new List { 2, 3 } }; + var command = new ReorderFields { ParentFieldId = 1, FieldIds = new List { 3, 2 } }; await ExecuteCreateAsync(); await ExecuteAddArrayFieldAsync(); await ExecuteAddFieldAsync("field1", 1); await ExecuteAddFieldAsync("field2", 1); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -348,9 +348,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(command.Properties, GetField(1).RawProperties); @@ -368,9 +368,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); await ExecuteAddArrayFieldAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); @@ -388,9 +388,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(command.Properties, GetField(1).RawProperties); @@ -409,9 +409,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteAddArrayFieldAsync(); await ExecuteAddFieldAsync(fieldName, 1); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); @@ -429,9 +429,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.False(GetField(1).IsDisabled); @@ -450,9 +450,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteAddArrayFieldAsync(); await ExecuteAddFieldAsync(fieldName, 1); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(GetNestedField(1, 2).IsLocked); @@ -470,9 +470,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(GetField(1).IsHidden); @@ -491,9 +491,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteAddArrayFieldAsync(); await ExecuteAddFieldAsync(fieldName, 1); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(GetNestedField(1, 2).IsHidden); @@ -512,9 +512,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteAddFieldAsync(fieldName); await ExecuteHideFieldAsync(1); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.False(GetField(1).IsHidden); @@ -534,9 +534,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteAddFieldAsync(fieldName, 1); await ExecuteHideFieldAsync(2, 1); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.False(GetNestedField(1, 2).IsHidden); @@ -554,9 +554,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(GetField(1).IsDisabled); @@ -575,9 +575,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteAddArrayFieldAsync(); await ExecuteAddFieldAsync(fieldName, 1); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(GetNestedField(1, 2).IsDisabled); @@ -596,9 +596,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteAddFieldAsync(fieldName); await ExecuteDisableFieldAsync(1); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.False(GetField(1).IsDisabled); @@ -618,9 +618,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteAddFieldAsync(fieldName, 1); await ExecuteDisableFieldAsync(2, 1); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.False(GetNestedField(1, 2).IsDisabled); @@ -638,9 +638,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Null(GetField(1)); @@ -659,9 +659,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteAddArrayFieldAsync(); await ExecuteAddFieldAsync(fieldName, 1); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Null(GetNestedField(1, 2)); @@ -681,9 +681,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(command.Category, sut.Snapshot.SchemaDef.Category); @@ -695,37 +695,37 @@ namespace Squidex.Domain.Apps.Entities.Schemas private Task ExecuteCreateAsync() { - return sut.ExecuteAsync(CreateCommand(new CreateSchema { Name = SchemaName })); + return PublishAsync(new CreateSchema { Name = SchemaName }); } private Task ExecuteAddArrayFieldAsync() { - return sut.ExecuteAsync(CreateCommand(new AddField { Properties = new ArrayFieldProperties(), Name = arrayName })); + return PublishAsync(new AddField { Properties = new ArrayFieldProperties(), Name = arrayName }); } private Task ExecuteAddFieldAsync(string name, long? parentId = null) { - return sut.ExecuteAsync(CreateCommand(new AddField { ParentFieldId = parentId, Properties = ValidProperties(), Name = name })); + return PublishAsync(new AddField { ParentFieldId = parentId, Properties = ValidProperties(), Name = name }); } private Task ExecuteHideFieldAsync(long id, long? parentId = null) { - return sut.ExecuteAsync(CreateCommand(new HideField { ParentFieldId = parentId, FieldId = id })); + return PublishAsync(new HideField { ParentFieldId = parentId, FieldId = id }); } private Task ExecuteDisableFieldAsync(long id, long? parentId = null) { - return sut.ExecuteAsync(CreateCommand(new DisableField { ParentFieldId = parentId, FieldId = id })); + return PublishAsync(new DisableField { ParentFieldId = parentId, FieldId = id }); } private Task ExecutePublishAsync() { - return sut.ExecuteAsync(CreateCommand(new PublishSchema())); + return PublishAsync(new PublishSchema()); } private Task ExecuteDeleteAsync() { - return sut.ExecuteAsync(CreateCommand(new DeleteSchema())); + return PublishAsync(new DeleteSchema()); } private IField GetField(int id) @@ -742,5 +742,27 @@ namespace Squidex.Domain.Apps.Entities.Schemas { return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; } + + private async Task PublishIdempotentAsync(SchemaCommand command) + { + var result = await PublishAsync(command); + + var previousSnapshot = sut.Snapshot; + var previousVersion = sut.Snapshot.Version; + + await PublishAsync(command); + + Assert.Same(previousSnapshot, sut.Snapshot); + Assert.Equal(previousVersion, sut.Snapshot.Version); + + return result; + } + + private async Task PublishAsync(SchemaCommand command) + { + var result = await sut.ExecuteAsync(CreateCommand(command)); + + return result.Value; + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs index fc35024a0..34a4c705f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; +using System.Linq; using FakeItEasy; using Squidex.Infrastructure.Queries; @@ -21,5 +23,15 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers { return values == null ? that.IsNull() : that.IsSameSequenceAs(values); } + + public static IEnumerable Has(this INegatableArgumentConstraintManager> that, params T[]? values) + { + return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Length); + } + + public static HashSet Has(this INegatableArgumentConstraintManager> that, params T[]? values) + { + return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Length); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs index e6b249f93..46859d9a0 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs @@ -37,6 +37,11 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers ((object)lhs).Should().BeEquivalentTo(rhs, o => o.IncludingAllRuntimeProperties()); } + public static void ShouldBeEquivalent2(this T lhs, T rhs) + { + lhs.Should().BeEquivalentTo(rhs, o => o.IncludingProperties()); + } + public static void ShouldBeEquivalent(this J lhs, T rhs) { lhs.Value.Should().BeEquivalentTo(rhs, o => o.IncludingProperties()); diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs index 6803b7948..2424c8470 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs @@ -124,6 +124,25 @@ namespace Squidex.Infrastructure.Commands Assert.Equal(1, sut.Snapshot.Version); } + [Fact] + public async Task Should_not_update_when_snapshot_is_not_changed() + { + await SetupCreatedAsync(); + + var previousSnapshot = sut.Snapshot; + + var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = MyDomainState.Unchanged })); + + Assert.True(result.Value is EntitySavedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + + Assert.Same(previousSnapshot, sut.Snapshot); + } + [Fact] public async Task Should_throw_exception_when_already_created() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs index 38d92a242..fc2a9495d 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs @@ -177,6 +177,25 @@ namespace Squidex.Infrastructure.Commands Assert.Equal(1, sut.Snapshot.Version); } + [Fact] + public async Task Should_not_update_when_snapshot_is_not_changed() + { + await SetupCreatedAsync(); + + var previousSnapshot = sut.Snapshot; + + var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = MyDomainState.Unchanged })); + + Assert.True(result.Value is EntitySavedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + + Assert.Same(previousSnapshot, sut.Snapshot); + } + [Fact] public async Task Should_throw_exception_when_already_created() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs index e6706d506..5ea811994 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs @@ -152,7 +152,7 @@ namespace Squidex.Infrastructure.Json.Objects } [Fact] - public void Should_boolean_from_object() + public void Should_create_boolean_from_object() { Assert.Equal(JsonValue.True, JsonValue.Create((object)true)); } @@ -353,5 +353,123 @@ namespace Squidex.Infrastructure.Json.Objects { Assert.Throws(() => JsonValue.Create(Guid.Empty)); } + + [Fact] + public void Should_return_null_when_getting_value_by_path_segment_from_null() + { + var json = JsonValue.Null; + + var found = json.TryGet("path", out var result); + + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void Should_return_null_when_getting_value_by_path_segment_from_string() + { + var json = JsonValue.Create("string"); + + var found = json.TryGet("path", out var result); + + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void Should_return_null_when_getting_value_by_path_segment_from_boolean() + { + var json = JsonValue.True; + + var found = json.TryGet("path", out var result); + + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void Should_return_null_when_getting_value_by_path_segment_from_number() + { + var json = JsonValue.Create(12); + + var found = json.TryGet("path", out var result); + + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void Should_return_same_object_when_path_is_null() + { + var json = JsonValue.Object().Add("property", 12); + + var found = json.TryGetByPath((string?)null, out var result); + + Assert.False(found); + Assert.Same(json, result); + } + + [Fact] + public void Should_return_same_object_when_path_is_empty() + { + var json = JsonValue.Object().Add("property", 12); + + var found = json.TryGetByPath(string.Empty, out var result); + + Assert.False(found); + Assert.Same(json, result); + } + + [Fact] + public void Should_return_from_nested_array() + { + var json = + JsonValue.Object() + .Add("property", + JsonValue.Array( + JsonValue.Create(12), + JsonValue.Object() + .Add("nested", 13))); + + var found = json.TryGetByPath("property[1].nested", out var result); + + Assert.True(found); + Assert.Equal(JsonValue.Create(13), result); + } + + [Fact] + public void Should_return_null_when_property_not_found() + { + var json = + JsonValue.Object() + .Add("property", 12); + + var found = json.TryGetByPath("notfound", out var result); + + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void Should_return_null_when_out_of_index1() + { + var json = JsonValue.Array(12, 14); + + var found = json.TryGetByPath("-1", out var result); + + Assert.False(found); + Assert.Null(result); + } + + [Fact] + public void Should_return_null_when_out_of_index2() + { + var json = JsonValue.Array(12, 14); + + var found = json.TryGetByPath("2", out var result); + + Assert.False(found); + Assert.Null(result); + } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs index 0d7dc6ad3..b1a8b66fe 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs @@ -23,7 +23,7 @@ namespace Squidex.Infrastructure.Queries public JsonQueryConversionTests() { - var nested = new JsonSchemaProperty { Title = "nested" }; + var nested = new JsonSchemaProperty { Title = "nested", Type = JsonObjectType.Object }; nested.Properties["property"] = new JsonSchemaProperty { diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs index be114009d..95c702b93 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using NJsonSchema; @@ -18,12 +19,26 @@ namespace Squidex.Infrastructure.Queries { public sealed class QueryJsonConversionTests { + private static readonly (string Operator, string Output)[] AllOps = + { + ("contains", "contains($FIELD, $VALUE)"), + ("empty", "empty($FIELD)"), + ("endswith", "endsWith($FIELD, $VALUE)"), + ("eq", "$FIELD == $VALUE"), + ("ge", "$FIELD >= $VALUE"), + ("gt", "$FIELD > $VALUE"), + ("le", "$FIELD <= $VALUE"), + ("lt", "$FIELD < $VALUE"), + ("ne", "$FIELD != $VALUE"), + ("startswith", "startsWith($FIELD, $VALUE)"), + }; + private readonly List errors = new List(); private readonly JsonSchema schema = new JsonSchema(); public QueryJsonConversionTests() { - var nested = new JsonSchemaProperty { Title = "nested" }; + var nested = new JsonSchemaProperty { Title = "nested", Type = JsonObjectType.Object }; nested.Properties["property"] = new JsonSchemaProperty { @@ -55,6 +70,11 @@ namespace Squidex.Infrastructure.Queries Type = JsonObjectType.Number }; + schema.Properties["json"] = new JsonSchemaProperty + { + Type = JsonObjectType.None + }; + schema.Properties["string"] = new JsonSchemaProperty { Type = JsonObjectType.String @@ -93,20 +113,18 @@ namespace Squidex.Infrastructure.Queries AssertErrors(json, "'notfound' is not a property of 'nested'."); } + public static IEnumerable DateTimeTests() + { + const string value = "2012-11-10T09:08:07Z"; + + return BuildTests("datetime", x => true, value, value); + } + [Theory] - [InlineData("contains", "contains(datetime, 2012-11-10T09:08:07Z)")] - [InlineData("empty", "empty(datetime)")] - [InlineData("endswith", "endsWith(datetime, 2012-11-10T09:08:07Z)")] - [InlineData("eq", "datetime == 2012-11-10T09:08:07Z")] - [InlineData("ge", "datetime >= 2012-11-10T09:08:07Z")] - [InlineData("gt", "datetime > 2012-11-10T09:08:07Z")] - [InlineData("le", "datetime <= 2012-11-10T09:08:07Z")] - [InlineData("lt", "datetime < 2012-11-10T09:08:07Z")] - [InlineData("ne", "datetime != 2012-11-10T09:08:07Z")] - [InlineData("startswith", "startsWith(datetime, 2012-11-10T09:08:07Z)")] - public void Should_parse_datetime_string_filter(string op, string expected) - { - var json = new { path = "datetime", op, value = "2012-11-10T09:08:07Z" }; + [MemberData(nameof(DateTimeTests))] + public void Should_parse_datetime_string_filter(string field, string op, string value, string expected) + { + var json = new { path = field, op, value }; AssertFilter(json, expected); } @@ -127,20 +145,18 @@ namespace Squidex.Infrastructure.Queries AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); } + public static IEnumerable GuidTests() + { + const string value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3"; + + return BuildTests("guid", x => true, value, value); + } + [Theory] - [InlineData("contains", "contains(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - [InlineData("empty", "empty(guid)")] - [InlineData("endswith", "endsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - [InlineData("eq", "guid == bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("ge", "guid >= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("gt", "guid > bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("le", "guid <= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("lt", "guid < bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("ne", "guid != bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("startswith", "startsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - public void Should_parse_guid_string_filter(string op, string expected) - { - var json = new { path = "guid", op, value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3" }; + [MemberData(nameof(GuidTests))] + public void Should_parse_guid_string_filter(string field, string op, string value, string expected) + { + var json = new { path = field, op, value }; AssertFilter(json, expected); } @@ -161,38 +177,44 @@ namespace Squidex.Infrastructure.Queries AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); } + public static IEnumerable StringTests() + { + const string value = "Hello"; + + return BuildTests("string", x => true, value, $"'{value}'"); + } + [Theory] - [InlineData("contains", "contains(string, 'Hello')")] - [InlineData("empty", "empty(string)")] - [InlineData("endswith", "endsWith(string, 'Hello')")] - [InlineData("eq", "string == 'Hello'")] - [InlineData("ge", "string >= 'Hello'")] - [InlineData("gt", "string > 'Hello'")] - [InlineData("le", "string <= 'Hello'")] - [InlineData("lt", "string < 'Hello'")] - [InlineData("ne", "string != 'Hello'")] - [InlineData("startswith", "startsWith(string, 'Hello')")] - public void Should_parse_string_filter(string op, string expected) - { - var json = new { path = "string", op, value = "Hello" }; + [MemberData(nameof(StringTests))] + public void Should_parse_string_filter(string field, string op, string value, string expected) + { + var json = new { path = field, op, value }; AssertFilter(json, expected); } - [Fact] - public void Should_add_error_if_string_property_got_invalid_value() + public static IEnumerable StringInTests() { - var json = new { path = "string", op = "eq", value = 1 }; + const string value = "Hello"; - AssertErrors(json, "Expected String for path 'string', but got Number."); + return BuildInTests("string", value, $"'{value}'"); + } + + [Theory] + [MemberData(nameof(StringInTests))] + public void Should_parse_string_in_filter(string field, string value, string expected) + { + var json = new { path = field, op = "in", value = new[] { value } }; + + AssertFilter(json, expected); } [Fact] - public void Should_parse_string_in_filter() + public void Should_add_error_if_string_property_got_invalid_value() { - var json = new { path = "string", op = "in", value = new[] { "Hello" } }; + var json = new { path = "string", op = "eq", value = 1 }; - AssertFilter(json, "string in ['Hello']"); + AssertErrors(json, "Expected String for path 'string', but got Number."); } [Fact] @@ -211,16 +233,34 @@ namespace Squidex.Infrastructure.Queries AssertFilter(json, "reference.property in ['Hello']"); } + public static IEnumerable NumberTests() + { + const int value = 12; + + return BuildTests("number", x => x.Length == 2, value, $"{value}"); + } + [Theory] - [InlineData("eq", "number == 12")] - [InlineData("ge", "number >= 12")] - [InlineData("gt", "number > 12")] - [InlineData("le", "number <= 12")] - [InlineData("lt", "number < 12")] - [InlineData("ne", "number != 12")] - public void Should_parse_number_filter(string op, string expected) + [MemberData(nameof(NumberTests))] + public void Should_parse_number_filter(string field, string op, int value, string expected) { - var json = new { path = "number", op, value = 12 }; + var json = new { path = field, op, value }; + + AssertFilter(json, expected); + } + + public static IEnumerable NumberInTests() + { + const int value = 12; + + return BuildInTests("number", value, $"{value}"); + } + + [Theory] + [MemberData(nameof(NumberInTests))] + public void Should_parse_number_in_filter(string field, int value, string expected) + { + var json = new { path = field, op = "in", value = new[] { value } }; AssertFilter(json, expected); } @@ -233,20 +273,34 @@ namespace Squidex.Infrastructure.Queries AssertErrors(json, "Expected Number for path 'number', but got Boolean."); } - [Fact] - public void Should_parse_number_in_filter() + public static IEnumerable BooleanTests() + { + const bool value = true; + + return BuildTests("boolean", x => x == "eq" || x == "ne", value, $"{value}"); + } + + [Theory] + [MemberData(nameof(BooleanTests))] + public void Should_parse_boolean_filter(string field, string op, bool value, string expected) + { + var json = new { path = field, op, value }; + + AssertFilter(json, expected); + } + + public static IEnumerable BooleanInTests() { - var json = new { path = "number", op = "in", value = new[] { 12 } }; + const bool value = true; - AssertFilter(json, "number in [12]"); + return BuildInTests("boolean", value, $"{value}"); } [Theory] - [InlineData("eq", "boolean == True")] - [InlineData("ne", "boolean != True")] - public void Should_parse_boolean_filter(string op, string expected) + [MemberData(nameof(BooleanInTests))] + public void Should_parse_boolean_in_filter(string field, bool value, string expected) { - var json = new { path = "boolean", op, value = true }; + var json = new { path = field, op = "in", value = new[] { value } }; AssertFilter(json, expected); } @@ -259,31 +313,36 @@ namespace Squidex.Infrastructure.Queries AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); } - [Fact] - public void Should_parse_boolean_in_filter() + public static IEnumerable ArrayTests() { - var json = new { path = "boolean", op = "in", value = new[] { true } }; + const string value = "Hello"; - AssertFilter(json, "boolean in [True]"); + return BuildTests("stringArray", x => x == "eq" || x == "ne" || x == "empty", value, $"'{value}'"); } [Theory] - [InlineData("empty", "empty(stringArray)")] - [InlineData("eq", "stringArray == 'Hello'")] - [InlineData("ne", "stringArray != 'Hello'")] - public void Should_parse_array_filter(string op, string expected) + [MemberData(nameof(ArrayTests))] + public void Should_parse_array_filter(string field, string op, string value, string expected) { - var json = new { path = "stringArray", op, value = "Hello" }; + var json = new { path = field, op, value }; AssertFilter(json, expected); } - [Fact] - public void Should_parse_array_in_filter() + public static IEnumerable ArrayInTests() + { + const string value = "Hello"; + + return BuildInTests("stringArray", value, $"'{value}'"); + } + + [Theory] + [MemberData(nameof(ArrayInTests))] + public void Should_parse_array_in_filter(string field, string value, string expected) { - var json = new { path = "stringArray", op = "in", value = new[] { "Hello" } }; + var json = new { path = field, op = "in", value = new[] { value } }; - AssertFilter(json, "stringArray in ['Hello']"); + AssertFilter(json, expected); } [Fact] @@ -370,5 +429,45 @@ namespace Squidex.Infrastructure.Queries return jsonFilter.ToString(); } + + public static IEnumerable BuildInTests(string field, object value, string valueString) + { + var fields = new string[] + { + $"{field}", + $"json.{field}", + $"json.nested.{field}" + }; + + foreach (var f in fields) + { + var expected = $"{f} in [{valueString}]"; + + yield return new object[] { f, value, expected }; + } + } + + public static IEnumerable BuildTests(string field, Predicate opFilter, object value, string valueString) + { + var fields = new string[] + { + $"{field}", + $"json.{field}", + $"json.nested.{field}" + }; + + foreach (var f in fields) + { + foreach (var op in AllOps.Where(x => opFilter(x.Operator))) + { + var expected = + op.Output + .Replace("$FIELD", f) + .Replace("$VALUE", valueString); + + yield return new object[] { f, op.Operator, value, expected }; + } + } + } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs index 27eed1a57..d6ee1821a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs @@ -36,6 +36,7 @@ namespace Squidex.Infrastructure.Queries entityType.AddStructuralProperty("incomeMioNullable", EdmPrimitiveTypeKind.Double, true); entityType.AddStructuralProperty("age", EdmPrimitiveTypeKind.Int32, false); entityType.AddStructuralProperty("ageNullable", EdmPrimitiveTypeKind.Int32, true); + entityType.AddStructuralProperty("properties", new EdmComplexTypeReference(new EdmComplexType("Squidex", "Properties", null, false, true), true)); var container = new EdmEntityContainer("Squidex", "Container"); @@ -60,6 +61,8 @@ namespace Squidex.Infrastructure.Queries [Theory] [InlineData("created")] [InlineData("createdNullable")] + [InlineData("properties/datetime")] + [InlineData("properties/nested/dateime")] public void Should_parse_filter_when_type_is_datetime(string field) { var i = Q($"$filter={field} eq 1988-01-19T12:00:00Z"); @@ -89,6 +92,8 @@ namespace Squidex.Infrastructure.Queries [Theory] [InlineData("birthday")] [InlineData("birthdayNullable")] + [InlineData("properties/date")] + [InlineData("properties/nested/date")] public void Should_parse_filter_when_type_is_date(string field) { var i = Q($"$filter={field} eq 1988-01-19"); @@ -109,6 +114,8 @@ namespace Squidex.Infrastructure.Queries [Theory] [InlineData("id")] [InlineData("idNullable")] + [InlineData("properties/uid")] + [InlineData("properties/nested/guid")] public void Should_parse_filter_when_type_is_guid(string field) { var i = Q($"$filter={field} eq B5FE25E3-B262-4B17-91EF-B3772A6B62BB"); @@ -138,6 +145,8 @@ namespace Squidex.Infrastructure.Queries [Theory] [InlineData("firstName")] [InlineData("firstNameNullable")] + [InlineData("properties/string")] + [InlineData("properties/nested/string")] public void Should_parse_filter_when_type_is_string(string field) { var i = Q($"$filter={field} eq 'Dagobert'"); @@ -158,6 +167,8 @@ namespace Squidex.Infrastructure.Queries [Theory] [InlineData("isComicFigure")] [InlineData("isComicFigureNullable")] + [InlineData("properties/boolean")] + [InlineData("properties/nested/boolean")] public void Should_parse_filter_when_type_is_boolean(string field) { var i = Q($"$filter={field} eq true"); @@ -178,6 +189,8 @@ namespace Squidex.Infrastructure.Queries [Theory] [InlineData("age")] [InlineData("ageNullable")] + [InlineData("properties/int")] + [InlineData("properties/nested/int")] public void Should_parse_filter_when_type_is_int32(string field) { var i = Q($"$filter={field} eq 60"); @@ -198,6 +211,8 @@ namespace Squidex.Infrastructure.Queries [Theory] [InlineData("incomeCents")] [InlineData("incomeCentsNullable")] + [InlineData("properties/long")] + [InlineData("properties/nested/long")] public void Should_parse_filter_when_type_is_int64(string field) { var i = Q($"$filter={field} eq 31543143513456789"); @@ -218,6 +233,8 @@ namespace Squidex.Infrastructure.Queries [Theory] [InlineData("incomeMio")] [InlineData("incomeMioNullable")] + [InlineData("properties/double")] + [InlineData("properties/nested/double")] public void Should_parse_filter_when_type_is_double(string field) { var i = Q($"$filter={field} eq 5634474356.1233"); @@ -444,7 +461,7 @@ namespace Squidex.Infrastructure.Queries private static string C(string value) { - return value; + return value.Replace('/', '.'); } private static string? Q(string value) diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs index f8c1593f8..9d96c2d89 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -60,21 +60,21 @@ namespace Squidex.Infrastructure.TestHelpers public sealed class CreateAuto : MyCommand { - public int Value { get; set; } + public long Value { get; set; } } public sealed class CreateCustom : MyCommand { - public int Value { get; set; } + public long Value { get; set; } } public sealed class UpdateAuto : MyCommand { - public int Value { get; set; } + public long Value { get; set; } } public sealed class UpdateCustom : MyCommand { - public int Value { get; set; } + public long Value { get; set; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs index 7af864c86..10c01335a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs @@ -12,18 +12,27 @@ namespace Squidex.Infrastructure.TestHelpers { public sealed class MyDomainState : IDomainState { + public const long Unchanged = 13; + public long Version { get; set; } - public int Value { get; set; } + public long Value { get; set; } public MyDomainState Apply(Envelope @event) { - return new MyDomainState { Value = ((ValueChanged)@event.Payload).Value }; + var value = @event.To().Payload.Value; + + if (value == Unchanged) + { + return this; + } + + return new MyDomainState { Value = value }; } } public sealed class ValueChanged : IEvent { - public int Value { get; set; } + public long Value { get; set; } } } diff --git a/backend/tools/Migrate_01/MigrationPath.cs b/backend/tools/Migrate_01/MigrationPath.cs index 6701d39ea..5bcf60f01 100644 --- a/backend/tools/Migrate_01/MigrationPath.cs +++ b/backend/tools/Migrate_01/MigrationPath.cs @@ -17,7 +17,7 @@ namespace Migrate_01 { public sealed class MigrationPath : IMigrationPath { - private const int CurrentVersion = 19; + private const int CurrentVersion = 20; private readonly IServiceProvider serviceProvider; public MigrationPath(IServiceProvider serviceProvider) @@ -114,8 +114,8 @@ namespace Migrate_01 yield return serviceProvider.GetService(); } - // Version 18: Rebuild assets. - if (version < 18) + // Version 20: Rebuild assets. + if (version < 20) { yield return serviceProvider.GetService(); } diff --git a/backend/tools/Migrate_01/OldEvents/AssetCreated.cs b/backend/tools/Migrate_01/OldEvents/AssetCreated.cs new file mode 100644 index 000000000..7b2b72bbe --- /dev/null +++ b/backend/tools/Migrate_01/OldEvents/AssetCreated.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Assets; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Reflection; +using AssetCreatedV2 = Squidex.Domain.Apps.Events.Assets.AssetCreated; + +namespace Migrate_01.OldEvents +{ + [EventType(nameof(AssetCreated))] + [Obsolete] + public sealed class AssetCreated : AssetEvent, IMigrated + { + public Guid ParentId { get; set; } + + public string FileName { get; set; } + + public string FileHash { get; set; } + + public string MimeType { get; set; } + + public string Slug { get; set; } + + public long FileVersion { get; set; } + + public long FileSize { get; set; } + + public bool IsImage { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } + + public HashSet? Tags { get; set; } + + public IEvent Migrate() + { + var result = SimpleMapper.Map(this, new AssetCreatedV2()); + + result.Metadata = new AssetMetadata(); + + if (IsImage && PixelWidth.HasValue && PixelHeight.HasValue) + { + result.Type = AssetType.Image; + + result.Metadata.SetPixelWidth(PixelWidth.Value); + result.Metadata.SetPixelHeight(PixelHeight.Value); + } + + return result; + } + } +} diff --git a/backend/tools/Migrate_01/OldEvents/AssetUpdated.cs b/backend/tools/Migrate_01/OldEvents/AssetUpdated.cs new file mode 100644 index 000000000..d2133ddde --- /dev/null +++ b/backend/tools/Migrate_01/OldEvents/AssetUpdated.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Assets; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Reflection; +using AssetUpdatedV2 = Squidex.Domain.Apps.Events.Assets.AssetUpdated; + +namespace Migrate_01.OldEvents +{ + [TypeName("AssetUpdated")] + public sealed class AssetUpdated : AssetEvent, IMigrated + { + public string MimeType { get; set; } + + public string FileHash { get; set; } + + public long FileSize { get; set; } + + public long FileVersion { get; set; } + + public bool IsImage { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } + + public IEvent Migrate() + { + var result = SimpleMapper.Map(this, new AssetUpdatedV2()); + + result.Metadata = new AssetMetadata(); + + if (IsImage && PixelWidth.HasValue && PixelHeight.HasValue) + { + result.Type = AssetType.Image; + + result.Metadata.SetPixelWidth(PixelWidth.Value); + result.Metadata.SetPixelHeight(PixelHeight.Value); + } + + return result; + } + } +} diff --git a/frontend/app/features/administration/pages/restore/restore-page.component.html b/frontend/app/features/administration/pages/restore/restore-page.component.html index 3f6105e1b..76ce5f32b 100644 --- a/frontend/app/features/administration/pages/restore/restore-page.component.html +++ b/frontend/app/features/administration/pages/restore/restore-page.component.html @@ -54,7 +54,7 @@
- +
diff --git a/frontend/app/features/administration/pages/users/user-page.component.ts b/frontend/app/features/administration/pages/users/user-page.component.ts index e5b555d16..34d18e095 100644 --- a/frontend/app/features/administration/pages/users/user-page.component.ts +++ b/frontend/app/features/administration/pages/users/user-page.component.ts @@ -46,7 +46,9 @@ export class UserPageComponent extends ResourceOwner implements OnInit { this.isEditable = !user || user.canUpdate; - this.userForm.load(user || { permissions: [] }); + const permissions: string[] = []; + + this.userForm.load(user || { permissions } ); this.userForm.setEnabled(this.isEditable); })); } diff --git a/frontend/app/features/administration/state/users.forms.ts b/frontend/app/features/administration/state/users.forms.ts index 95e360b90..2e2ed5ba6 100644 --- a/frontend/app/features/administration/state/users.forms.ts +++ b/frontend/app/features/administration/state/users.forms.ts @@ -2,9 +2,9 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Form, ValidatorsEx } from '@app/shared'; -import { UpdateUserDto } from './../services/users.service'; +import { UpdateUserDto, UserDto } from './../services/users.service'; -export class UserForm extends Form { +export class UserForm extends Form { constructor( formBuilder: FormBuilder ) { @@ -36,7 +36,7 @@ export class UserForm extends Form { })); } - public load(value: any) { + public load(value: Partial) { if (value) { this.form.controls['password'].setValidators(Validators.nullValidator); } else { @@ -46,11 +46,13 @@ export class UserForm extends Form { super.load(value); } - protected transformLoad(user: UpdateUserDto) { - return { ...user, permissions: user.permissions.join('\n') }; + protected transformLoad(user: Partial) { + const permissions = user.permissions ? user.permissions.join('\n') : ''; + + return { ...user, permissions: permissions }; } - protected transformSubmit(value: any): UpdateUserDto { + protected transformSubmit(value: any) { return { ...value, permissions: value['permissions'].split('\n').filter((x: any) => !!x) }; } } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/field.component.ts b/frontend/app/features/schemas/pages/schema/field.component.ts index 528edd8d0..b05987ddd 100644 --- a/frontend/app/features/schemas/pages/schema/field.component.ts +++ b/frontend/app/features/schemas/pages/schema/field.component.ts @@ -12,7 +12,6 @@ import { FormBuilder } from '@angular/forms'; import { createProperties, DialogModel, - DialogService, EditFieldForm, fadeAnimation, ModalModel, @@ -57,7 +56,6 @@ export class FieldComponent implements OnChanges { public addFieldDialog = new DialogModel(); constructor( - private readonly dialogs: DialogService, private readonly formBuilder: FormBuilder, private readonly schemasState: SchemasState ) { @@ -121,8 +119,6 @@ export class FieldComponent implements OnChanges { this.schemasState.updateField(this.schema, this.field, { properties }) .subscribe(() => { this.editForm.submitCompleted(); - - this.dialogs.notifyInfo('Field saved successfully.'); }, error => { this.editForm.submitFailed(error); }); diff --git a/frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts index e1cfd177d..ccc511566 100644 --- a/frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts +++ b/frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts @@ -9,7 +9,6 @@ import { Component, Input, OnChanges } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { - DialogService, EditSchemaForm, SchemaDetailsDto, SchemasState @@ -31,7 +30,6 @@ export class SchemaEditFormComponent implements OnChanges { public isEditable = false; constructor( - private readonly dialogs: DialogService, private readonly formBuilder: FormBuilder, private readonly schemasState: SchemasState ) { @@ -54,8 +52,6 @@ export class SchemaEditFormComponent implements OnChanges { if (value) { this.schemasState.update(this.schema, value) .subscribe(() => { - this.dialogs.notifyInfo('Schema saved successfully.'); - this.editForm.submitCompleted({ noReset: true }); }, error => { this.editForm.submitFailed(error); diff --git a/frontend/app/features/schemas/pages/schema/schema-export-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-export-form.component.ts index 46e95a935..730a46867 100644 --- a/frontend/app/features/schemas/pages/schema/schema-export-form.component.ts +++ b/frontend/app/features/schemas/pages/schema/schema-export-form.component.ts @@ -9,7 +9,6 @@ import { Component, Input, OnChanges } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { - DialogService, SchemaDetailsDto, SchemasState, SynchronizeSchemaForm @@ -29,7 +28,6 @@ export class SchemaExportFormComponent implements OnChanges { public isEditable = false; constructor( - private readonly dialogs: DialogService, private readonly formBuilder: FormBuilder, private readonly schemasState: SchemasState ) { @@ -38,7 +36,7 @@ export class SchemaExportFormComponent implements OnChanges { public ngOnChanges() { this.isEditable = this.schema.canUpdateScripts; - this.synchronizeForm.form.get('json')!.setValue(this.schema.export()); + this.synchronizeForm.loadSchema(this.schema); } public synchronize() { @@ -49,16 +47,8 @@ export class SchemaExportFormComponent implements OnChanges { const value = this.synchronizeForm.submit(); if (value) { - const request = { - ...value.json, - noFieldDeletion: !value.fieldsDelete, - noFieldRecreation: !value.fieldsDelete - }; - - this.schemasState.synchronize(this.schema, request) + this.schemasState.synchronize(this.schema, value) .subscribe(() => { - this.dialogs.notifyInfo('Schema synchronized successfully.'); - this.synchronizeForm.submitCompleted({ noReset: true }); }, error => { this.synchronizeForm.submitFailed(error); diff --git a/frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts index 2c9ce97b0..2f9f75200 100644 --- a/frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts +++ b/frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts @@ -11,7 +11,6 @@ import { FormBuilder } from '@angular/forms'; import { AddPreviewUrlForm, ConfigurePreviewUrlsForm, - DialogService, SchemaDetailsDto, SchemasState } from '@app/shared'; @@ -32,7 +31,6 @@ export class SchemaPreviewUrlsFormComponent implements OnChanges { public isEditable = false; constructor( - private readonly dialogs: DialogService, private readonly formBuilder: FormBuilder, private readonly schemasState: SchemasState ) { @@ -41,7 +39,7 @@ export class SchemaPreviewUrlsFormComponent implements OnChanges { public ngOnChanges() { this.isEditable = this.schema.canUpdateUrls; - this.editForm.load(this.schema.previewUrls); + this.editForm.load(this.schema); this.editForm.setEnabled(this.isEditable); } @@ -72,9 +70,7 @@ export class SchemaPreviewUrlsFormComponent implements OnChanges { if (value) { this.schemasState.configurePreviewUrls(this.schema, value) - .subscribe(() => { - this.dialogs.notifyInfo('Preview URLs successfully.'); - + .subscribe(update => { this.editForm.submitCompleted({ noReset: true }); }, error => { this.editForm.submitFailed(error); diff --git a/frontend/app/features/schemas/pages/schema/schema-scripts-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-scripts-form.component.ts index 9a97e0e0c..2e438cf5e 100644 --- a/frontend/app/features/schemas/pages/schema/schema-scripts-form.component.ts +++ b/frontend/app/features/schemas/pages/schema/schema-scripts-form.component.ts @@ -9,7 +9,6 @@ import { Component, Input, OnChanges } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { - DialogService, EditScriptsForm, SchemaDetailsDto, SchemasState @@ -31,7 +30,6 @@ export class SchemaScriptsFormComponent implements OnChanges { public isEditable = false; constructor( - private readonly dialogs: DialogService, private readonly formBuilder: FormBuilder, private readonly schemasState: SchemasState ) { @@ -58,8 +56,6 @@ export class SchemaScriptsFormComponent implements OnChanges { if (value) { this.schemasState.configureScripts(this.schema, value) .subscribe(() => { - this.dialogs.notifyInfo('Scripts saved successfully.'); - this.editForm.submitCompleted({ noReset: true }); }, error => { this.editForm.submitFailed(error); diff --git a/frontend/app/features/schemas/pages/schema/schema-ui-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-ui-form.component.ts index 0e8be33ca..48b5afcec 100644 --- a/frontend/app/features/schemas/pages/schema/schema-ui-form.component.ts +++ b/frontend/app/features/schemas/pages/schema/schema-ui-form.component.ts @@ -7,11 +7,7 @@ import { Component, Input, OnChanges } from '@angular/core'; -import { - DialogService, - SchemaDetailsDto, - SchemasState -} from '@app/shared'; +import { SchemaDetailsDto, SchemasState } from '@app/shared'; type State = { fieldsInLists: ReadonlyArray, fieldsInReferences: ReadonlyArray }; @@ -35,7 +31,6 @@ export class SchemaUIFormComponent implements OnChanges { }; constructor( - private readonly dialogs: DialogService, private readonly schemasState: SchemasState ) { } @@ -66,9 +61,6 @@ export class SchemaUIFormComponent implements OnChanges { return; } - this.schemasState.configureUIFields(this.schema, this.state) - .subscribe(() => { - this.dialogs.notifyInfo('UI fields updated successfully.'); - }); + this.schemasState.configureUIFields(this.schema, this.state); } } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schemas/schema-form.component.ts b/frontend/app/features/schemas/pages/schemas/schema-form.component.ts index d6f7f53bb..1bc12ccc0 100644 --- a/frontend/app/features/schemas/pages/schemas/schema-form.component.ts +++ b/frontend/app/features/schemas/pages/schemas/schema-form.component.ts @@ -44,7 +44,7 @@ export class SchemaFormComponent implements OnInit { } public ngOnInit() { - this.createForm.load({ name: '', import: this.import }); + this.createForm.load({ ...this.import, name: '' }); this.showImport = !!this.import; } @@ -67,9 +67,7 @@ export class SchemaFormComponent implements OnInit { const value = this.createForm.submit(); if (value) { - const schemaDto = Object.assign(value.import || {}, { name: value.name, isSingleton: value.isSingleton }); - - this.schemasState.create(schemaDto) + this.schemasState.create(value) .subscribe(dto => { this.emitComplete(dto); }, error => { diff --git a/frontend/app/features/settings/pages/clients/client-add-form.component.ts b/frontend/app/features/settings/pages/clients/client-add-form.component.ts index 3cc7e0e0d..8bb0c3c1a 100644 --- a/frontend/app/features/settings/pages/clients/client-add-form.component.ts +++ b/frontend/app/features/settings/pages/clients/client-add-form.component.ts @@ -19,10 +19,10 @@ import { AddClientForm, ClientsState } from '@app/shared';
- +
- +
@@ -45,7 +45,7 @@ export class ClientAddFormComponent { const value = this.addClientForm.submit(); if (value) { - this.clientsState.attach({ id: value.name }) + this.clientsState.attach(value) .subscribe(() => { this.addClientForm.submitCompleted(); }, error => { diff --git a/frontend/app/features/settings/pages/roles/role.component.ts b/frontend/app/features/settings/pages/roles/role.component.ts index b8f051621..68605dcff 100644 --- a/frontend/app/features/settings/pages/roles/role.component.ts +++ b/frontend/app/features/settings/pages/roles/role.component.ts @@ -12,7 +12,7 @@ import { AddPermissionForm, AutocompleteComponent, AutocompleteSource, - EditPermissionsForm, + EditRoleForm, RoleDto, RolesState } from '@app/shared'; @@ -47,7 +47,7 @@ export class RoleComponent implements OnChanges { public addPermissionForm = new AddPermissionForm(this.formBuilder); - public editForm = new EditPermissionsForm(); + public editForm = new EditRoleForm(); constructor( private readonly formBuilder: FormBuilder, @@ -58,7 +58,7 @@ export class RoleComponent implements OnChanges { public ngOnChanges() { this.isEditable = this.role.canUpdate; - this.editForm.load(this.role.permissions); + this.editForm.load(this.role); this.editForm.setEnabled(this.isEditable); } @@ -89,9 +89,7 @@ export class RoleComponent implements OnChanges { const value = this.editForm.submit(); if (value) { - const request = { permissions: value }; - - this.rolesState.update(this.role, request) + this.rolesState.update(this.role, value) .subscribe(() => { this.editForm.submitCompleted(); diff --git a/frontend/app/framework/angular/pipes/numbers.pipes.ts b/frontend/app/framework/angular/pipes/numbers.pipes.ts index 420c24dec..a4bd18fb3 100644 --- a/frontend/app/framework/angular/pipes/numbers.pipes.ts +++ b/frontend/app/framework/angular/pipes/numbers.pipes.ts @@ -37,13 +37,18 @@ export class KNumberPipe implements PipeTransform { }) export class FileSizePipe implements PipeTransform { public transform(value: number) { - let u = 0, s = 1024; + return calculateFileSize(value); + } +} - while (value >= s || -value >= s) { - value /= s; - u++; - } +export function calculateFileSize(value: number) { + let u = 0, s = 1024; - return (u ? value.toFixed(1) + ' ' : value) + ' kMGTPEZY'[u] + 'B'; + while (value >= s || -value >= s) { + value /= s; + u++; } + + return (u ? value.toFixed(1) + ' ' : value) + ' kMGTPEZY'[u] + 'B'; + } \ No newline at end of file diff --git a/frontend/app/framework/state.ts b/frontend/app/framework/state.ts index 131d9d372..049fb8988 100644 --- a/frontend/app/framework/state.ts +++ b/frontend/app/framework/state.ts @@ -15,13 +15,17 @@ import { ErrorDto } from './utils/error'; import { ResourceLinks } from './utils/hateos'; import { Types } from './utils/types'; +export type Mutable = { + -readonly [P in keyof T ]: T[P] +}; + export interface FormState { submitted: boolean; error?: ErrorDto | null; } -export class Form { +export class Form { private readonly state = new State({ submitted: false }); public submitted = @@ -51,7 +55,7 @@ export class Form { this.form.disable(); } - protected setValue(value?: V) { + protected setValue(value?: Partial) { if (value) { this.form.reset(this.transformLoad(value)); } else { @@ -59,21 +63,21 @@ export class Form { } } - protected transformLoad(value: V): any { + protected transformLoad(value: Partial): any { return value; } - protected transformSubmit(value: any): V { + protected transformSubmit(value: any): TOut { return value; } - public load(value: V | undefined) { + public load(value: Partial | undefined) { this.state.next({ submitted: false, error: null }); this.setValue(value); } - public submit(): V | null { + public submit(): TOut | null { this.state.next({ submitted: true, error: null }); if (this.form.valid) { @@ -89,7 +93,7 @@ export class Form { } } - public submitCompleted(options?: { newValue?: V, noReset?: boolean }) { + public submitCompleted(options?: { newValue?: TOut, noReset?: boolean }) { this.state.next({ submitted: false, error: null }); this.enable(); diff --git a/frontend/app/shared/components/asset-dialog.component.html b/frontend/app/shared/components/asset-dialog.component.html index 81ce3a59a..ef86fe842 100644 --- a/frontend/app/shared/components/asset-dialog.component.html +++ b/frontend/app/shared/components/asset-dialog.component.html @@ -34,6 +34,40 @@
+ +
+ + +
+
+ + + +
+ +
+ + + +
+ +
+ +
+
+ +
+ +
+
diff --git a/frontend/app/shared/components/asset-dialog.component.ts b/frontend/app/shared/components/asset-dialog.component.ts index e06c07f3a..658e16e64 100644 --- a/frontend/app/shared/components/asset-dialog.component.ts +++ b/frontend/app/shared/components/asset-dialog.component.ts @@ -69,6 +69,8 @@ export class AssetDialogComponent implements OnInit { }, error => { this.annotateForm.submitFailed(error); }); + } else if (this.annotateForm.form.valid) { + this.emitComplete(); } } } \ No newline at end of file diff --git a/frontend/app/shared/components/asset-folder-form.component.ts b/frontend/app/shared/components/asset-folder-form.component.ts index 41065601a..45d561c99 100644 --- a/frontend/app/shared/components/asset-folder-form.component.ts +++ b/frontend/app/shared/components/asset-folder-form.component.ts @@ -10,8 +10,8 @@ import { FormBuilder } from '@angular/forms'; import { AssetFolderDto, - AssetFolderForm, - AssetsState + AssetsState, + RenameAssetFolderForm } from '@app/shared/internal'; @Component({ @@ -27,7 +27,7 @@ export class AssetFolderFormComponent implements OnInit { @Input() public assetFolder: AssetFolderDto; - public editForm = new AssetFolderForm(this.formBuilder); + public editForm = new RenameAssetFolderForm(this.formBuilder); constructor( private readonly assetsState: AssetsState, @@ -37,7 +37,7 @@ export class AssetFolderFormComponent implements OnInit { public ngOnInit() { if (this.assetFolder) { - this.editForm.load({ folderName: this.assetFolder.folderName }); + this.editForm.load(this.assetFolder); } } diff --git a/frontend/app/shared/components/asset.component.html b/frontend/app/shared/components/asset.component.html index 9e29768da..01e0cbe9a 100644 --- a/frontend/app/shared/components/asset.component.html +++ b/frontend/app/shared/components/asset.component.html @@ -76,7 +76,7 @@
- {{asset.pixelWidth}}x{{asset.pixelHeight}}px, {{asset.fileSize | sqxFileSize}} + {{asset.metadataText}}
@@ -112,7 +112,7 @@ - {{asset.pixelWidth}}x{{asset.pixelHeight}}px, {{asset.fileSize | sqxFileSize}} + {{asset.metadataText}} diff --git a/frontend/app/shared/services/apps.service.spec.ts b/frontend/app/shared/services/apps.service.spec.ts index 31c7c49ad..a8c98a1e6 100644 --- a/frontend/app/shared/services/apps.service.spec.ts +++ b/frontend/app/shared/services/apps.service.spec.ts @@ -262,5 +262,5 @@ export function createApp(id: number, suffix = '') { id % 2 === 0, id % 2 === 0, 'Free', 'Basic', - new Version(`${id}`)); + new Version(`${id}${suffix}`)); } \ No newline at end of file diff --git a/frontend/app/shared/services/assets.service.spec.ts b/frontend/app/shared/services/assets.service.spec.ts index 10a171851..40202c33c 100644 --- a/frontend/app/shared/services/assets.service.spec.ts +++ b/frontend/app/shared/services/assets.service.spec.ts @@ -418,9 +418,12 @@ describe('AssetsService', () => { fileVersion: id * 4, parentId, mimeType: 'image/png', - isImage: true, - pixelWidth: id * 3, - pixelHeight: id * 5, + type: `my-type${id}${suffix}`, + metadataText: `my-metadata${id}${suffix}`, + metadata: { + pixelWidth: id * 3, + pixelHeight: id * 5 + }, slug: `my-name${id}${suffix}.png`, tags: ['tag1', 'tag2'], version: id, @@ -471,12 +474,15 @@ export function createAsset(id: number, tags?: ReadonlyArray, suffix = ' id * 4, parentId, 'image/png', - true, - id * 3, - id * 5, + `my-type${id}${suffix}`, + `my-metadata${id}${suffix}`, + { + pixelWidth: id * 3, + pixelHeight: id * 5 + }, `my-name${id}${suffix}.png`, tags || ['tag1', 'tag2'], - new Version(`${id}`)); + new Version(`${id}${suffix}`)); } export function createAssetFolder(id: number, suffix = '', parentId?: string) { diff --git a/frontend/app/shared/services/assets.service.ts b/frontend/app/shared/services/assets.service.ts index 8190a725e..1592c7e46 100644 --- a/frontend/app/shared/services/assets.service.ts +++ b/frontend/app/shared/services/assets.service.ts @@ -27,6 +27,8 @@ import { Versioned } from '@app/framework'; +const SVG_PREVIEW_LIMIT = 10 * 1024; + import { encodeQuery, Query } from './../state/query'; export class AssetsDto extends ResultSet { @@ -67,14 +69,14 @@ export class AssetDto { public readonly fileVersion: number, public readonly parentId: string, public readonly mimeType: string, - public readonly isImage: boolean, - public readonly pixelWidth: number | null | undefined, - public readonly pixelHeight: number | null | undefined, + public readonly type: string, + public readonly metadataText: string, + public readonly metadata: any, public readonly slug: string, public readonly tags: ReadonlyArray, public readonly version: Version ) { - this.canPreview = this.isImage || (this.mimeType === 'image/svg+xml' && this.fileSize < 100 * 1024); + this.canPreview = this.type === 'Image' || (this.mimeType === 'image/svg+xml' && this.fileSize < SVG_PREVIEW_LIMIT); this._links = links; @@ -122,6 +124,7 @@ export interface AnnotateAssetDto { readonly fileName?: string; readonly slug?: string; readonly tags?: ReadonlyArray; + readonly metadata?: { [key: string]: any }; } export interface CreateAssetFolderDto { @@ -376,9 +379,9 @@ function parseAsset(response: any) { response.fileVersion, response.parentId, response.mimeType, - response.isImage, - response.pixelWidth, - response.pixelHeight, + response.type, + response.metadataText, + response.metadata, response.slug, response.tags || [], new Version(response.version.toString())); diff --git a/frontend/app/shared/services/contents.service.spec.ts b/frontend/app/shared/services/contents.service.spec.ts index 9720a1560..27a57724b 100644 --- a/frontend/app/shared/services/contents.service.spec.ts +++ b/frontend/app/shared/services/contents.service.spec.ts @@ -430,5 +430,5 @@ export function createContent(id: number, suffix = '') { 'MySchema', {}, [], - new Version(`${id}`)); + new Version(`${id}${suffix}`)); } \ No newline at end of file diff --git a/frontend/app/shared/services/rules.service.spec.ts b/frontend/app/shared/services/rules.service.spec.ts index 08981dba7..716cf040b 100644 --- a/frontend/app/shared/services/rules.service.spec.ts +++ b/frontend/app/shared/services/rules.service.spec.ts @@ -426,7 +426,7 @@ export function createRule(id: number, suffix = '') { `id${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier${id}`, - new Version(`${id}`), + new Version(`${id}${suffix}`), id % 2 === 0, { param1: 1, diff --git a/frontend/app/shared/services/schemas.service.spec.ts b/frontend/app/shared/services/schemas.service.spec.ts index f6c6b9ebe..987cea297 100644 --- a/frontend/app/shared/services/schemas.service.spec.ts +++ b/frontend/app/shared/services/schemas.service.spec.ts @@ -808,7 +808,7 @@ export function createSchema(id: number, suffix = '') { id % 3 === 0, DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier${id}`, - new Version(`${id}`)); + new Version(`${id}${suffix}`)); } export function createSchemaDetails(id: number, suffix = '') { @@ -825,7 +825,7 @@ export function createSchemaDetails(id: number, suffix = '') { id % 3 === 0, DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier${id}`, - new Version(`${id}`), + new Version(`${id}${suffix}`), [ new RootFieldDto({}, 11, 'field11', createProperties('Array'), 'language', true, true, true, [ new NestedFieldDto({}, 101, 'field101', createProperties('String'), 11, true, true, true), diff --git a/frontend/app/shared/services/schemas.service.ts b/frontend/app/shared/services/schemas.service.ts index 6d49f978c..aefdb1d66 100644 --- a/frontend/app/shared/services/schemas.service.ts +++ b/frontend/app/shared/services/schemas.service.ts @@ -359,6 +359,7 @@ export interface SynchronizeSchemaDto { export interface UpdateSchemaDto { readonly label?: string; readonly hints?: string; + readonly tags?: ReadonlyArray; } @Injectable() diff --git a/frontend/app/shared/state/apps.forms.ts b/frontend/app/shared/state/apps.forms.ts index 6476ccb4b..282831b4d 100644 --- a/frontend/app/shared/state/apps.forms.ts +++ b/frontend/app/shared/state/apps.forms.ts @@ -9,7 +9,13 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Form, ValidatorsEx } from '@app/framework'; -export class CreateAppForm extends Form { +import { + AppDto, + CreateAppDto, + UpdateAppDto +} from './../services/apps.service'; + +export class CreateAppForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ name: ['', @@ -23,7 +29,7 @@ export class CreateAppForm extends Form { } } -export class UpdateAppForm extends Form { +export class UpdateAppForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ label: ['', diff --git a/frontend/app/shared/state/assets.forms.spec.ts b/frontend/app/shared/state/assets.forms.spec.ts new file mode 100644 index 000000000..b74824ff9 --- /dev/null +++ b/frontend/app/shared/state/assets.forms.spec.ts @@ -0,0 +1,106 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { FormBuilder } from '@angular/forms'; + +import { AnnotateAssetForm } from './assets.forms'; + +describe('AnnotateAssetForm', () => { + let form: AnnotateAssetForm; + + const asset: any = { + slug: 'my-file.png', + tags: [ + 'Tag1', + 'Tag2' + ], + metadata: { + key1: null, + key2: 'String', + key3: 13, + key4: true + }, + fileName: 'My File.png' + }; + + beforeEach(() => { + form = new AnnotateAssetForm(new FormBuilder()); + }); + + it('Should remov extension when loading asset file name', () => { + form.load(asset); + + const slug = form.form.get('fileName')!.value; + + expect(slug).toBe('My File'); + }); + + it('Should create slug from file name', () => { + form.load(asset); + form.generateSlug({} as any); + + const slug = form.form.get('slug')!.value; + + expect(slug).toBe('my-file'); + }); + + it('Should create slug from file name and append extension', () => { + form.form.get('fileName')!.setValue('My New File'); + form.generateSlug(asset); + + const slug = form.form.get('slug')!.value; + + expect(slug).toBe('my-new-file.png'); + }); + + it('Should convert metadata when loading', () => { + form.load(asset); + + const metadata = form.metadata.value; + + expect(metadata).toEqual([ + { name: 'key1', value: '' }, + { name: 'key2', value: 'String' }, + { name: 'key3', value: '13' }, + { name: 'key4', value: 'true' } + ]); + }); + + it('Should convert values when submitting', () => { + form.load(asset); + + const request = form.submit({ fileName: 'Old File.png' } as any)!; + + expect(request).toEqual(asset); + expect(form.form.enabled).toBeFalsy(); + }); + + it('Should return null when nothing changed before submit', () => { + form.load(asset); + + const result = form.submit(asset); + + expect(result).toBeNull(); + expect(form.form.enabled).toBeTruthy(); + }); + + it('Should remove previous metadata when loaded', () => { + const newAsset: any = { + metadata: { + key1: 'Value' + } + }; + + form.load(newAsset); + + const metadata = form.metadata.value; + + expect(metadata).toEqual([ + { name: 'key1', value: 'Value' } + ]); + }); +}); \ No newline at end of file diff --git a/frontend/app/shared/state/assets.forms.ts b/frontend/app/shared/state/assets.forms.ts index 9b65ea159..68a8b29dd 100644 --- a/frontend/app/shared/state/assets.forms.ts +++ b/frontend/app/shared/state/assets.forms.ts @@ -5,16 +5,31 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import slugify from 'slugify'; -import { Form, Types } from '@app/framework'; - -import { AssetDto } from './../services/assets.service'; +import { + Form, + Mutable, + Types +} from '@app/framework'; + +import { + AnnotateAssetDto, + AssetDto, + AssetFolderDto, + RenameAssetFolderDto +} from './../services/assets.service'; + +export class AnnotateAssetForm extends Form { + public get metadata() { + return this.form.get('metadata')! as FormArray; + } -export class AnnotateAssetForm extends Form }> { - constructor(formBuilder: FormBuilder) { + constructor( + private readonly formBuilder: FormBuilder + ) { super(formBuilder.group({ fileName: ['', [ @@ -26,16 +41,55 @@ export class AnnotateAssetForm extends Form | null = super.submit(); if (asset && result) { const index = asset.fileName.lastIndexOf('.'); @@ -52,6 +106,10 @@ export class AnnotateAssetForm extends Form) { + const result = { ...value }; - if (fileName) { - let slug = slugify(fileName, { lower: true }); + let fileName = value.fileName; - const index = asset.fileName.lastIndexOf('.'); + if (fileName) { + const index = fileName.lastIndexOf('.'); if (index > 0) { - slug += asset.fileName.substr(index); + fileName = fileName.substr(0, index); } - this.form.get('slug')!.setValue(slug); + result.fileName = fileName; } - } - public load(asset: AssetDto) { - let fileName = asset.fileName; + if (Types.isObject(value.metadata)) { + const length = Object.keys(value.metadata).length; + + while (this.metadata.controls.length < length) { + this.addMetadata(); + } + + while (this.metadata.controls.length > length) { + this.removeMetadata(this.metadata.controls.length - 1); + } + + result.metadata = []; + + for (const name in value.metadata) { + if (value.metadata.hasOwnProperty(name)) { + const raw = value.metadata[name]; - const index = fileName.lastIndexOf('.'); + let converted = ''; - if (index > 0) { - fileName = fileName.substr(0, index); + if (Types.isString(raw)) { + converted = raw; + } else if (!Types.isUndefined(raw) && !Types.isNull(raw)) { + converted = JSON.stringify(raw); + } + + result.metadata.push({ name, value: converted }); + } + } } - super.load({ fileName, slug: asset.slug, tags: asset.tags }); + return result; + } + + public generateSlug(asset: AssetDto) { + const fileName = this.form.get('fileName')!.value; + + if (fileName) { + let slug = slugify(fileName, { lower: true }); + + if (asset.fileName) { + const index = asset.fileName.lastIndexOf('.'); + + if (index > 0) { + slug += asset.fileName.substr(index); + } + } + + this.form.get('slug')!.setValue(slug); + } } } -export class AssetFolderForm extends Form { +export class RenameAssetFolderForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ folderName: ['', diff --git a/frontend/app/shared/state/backups.forms.ts b/frontend/app/shared/state/backups.forms.ts index 3decfeff7..7c365e636 100644 --- a/frontend/app/shared/state/backups.forms.ts +++ b/frontend/app/shared/state/backups.forms.ts @@ -13,12 +13,14 @@ import { ValidatorsEx } from '@app/framework'; -export class RestoreForm extends Form { +import { StartRestoreDto } from './../services/backups.service'; + +export class RestoreForm extends Form { public hasNoUrl = hasNoValue$(this.form.controls['url']); constructor(formBuilder: FormBuilder) { super(formBuilder.group({ - name: ['', + newAppName: ['', [ Validators.maxLength(40), ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'Name can contain lower case letters (a-z), numbers and dashes (not at the end).') diff --git a/frontend/app/shared/state/clients.forms.ts b/frontend/app/shared/state/clients.forms.ts index 2eedbce15..190a01cf2 100644 --- a/frontend/app/shared/state/clients.forms.ts +++ b/frontend/app/shared/state/clients.forms.ts @@ -13,7 +13,13 @@ import { ValidatorsEx } from '@app/framework'; -export class RenameClientForm extends Form { +import { + ClientDto, + CreateClientDto, + UpdateClientDto +} from './../services/clients.service'; + +export class RenameClientForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ name: ['', @@ -25,12 +31,12 @@ export class RenameClientForm extends Form { } } -export class AddClientForm extends Form { - public hasNoName = hasNoValue$(this.form.controls['name']); +export class AddClientForm extends Form { + public hasNoId = hasNoValue$(this.form.controls['id']); constructor(formBuilder: FormBuilder) { super(formBuilder.group({ - name: ['', + id: ['', [ Validators.maxLength(40), ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'Name can contain lower case letters (a-z), numbers and dashes between.') diff --git a/frontend/app/shared/state/comments.form.ts b/frontend/app/shared/state/comments.form.ts index e0e3ef0a9..c7c60eec1 100644 --- a/frontend/app/shared/state/comments.form.ts +++ b/frontend/app/shared/state/comments.form.ts @@ -9,7 +9,9 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { Form } from '@app/framework'; -export class UpsertCommentForm extends Form { +import { UpsertCommentDto } from './../services/comments.service'; + +export class UpsertCommentForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ text: '' diff --git a/frontend/app/shared/state/contents.forms.ts b/frontend/app/shared/state/contents.forms.ts index 9f03668f0..9d8dcd16e 100644 --- a/frontend/app/shared/state/contents.forms.ts +++ b/frontend/app/shared/state/contents.forms.ts @@ -45,7 +45,9 @@ export class HtmlValue { } } -export class SaveQueryForm extends Form { +type SaveQueryFormType = { name: string, user: boolean }; + +export class SaveQueryForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ name: ['', diff --git a/frontend/app/shared/state/contents.state.ts b/frontend/app/shared/state/contents.state.ts index 163cfed2d..32025ef2d 100644 --- a/frontend/app/shared/state/contents.state.ts +++ b/frontend/app/shared/state/contents.state.ts @@ -267,9 +267,7 @@ export abstract class ContentsStateBase extends State { public publishDraft(content: ContentDto, dueTime: string | null): Observable { return this.contentsService.publishDraft(this.appName, content, dueTime, content.version).pipe( tap(updated => { - this.dialogs.notifyInfo('Content updated successfully.'); - - this.replaceContent(updated); + this.replaceContent(updated, content.version, 'Content updated successfully.'); }), shareSubscribed(this.dialogs)); } @@ -277,9 +275,7 @@ export abstract class ContentsStateBase extends State { public changeStatus(content: ContentDto, status: string, dueTime: string | null): Observable { return this.contentsService.putStatus(this.appName, content, status, dueTime, content.version).pipe( tap(updated => { - this.dialogs.notifyInfo('Content updated successfully.'); - - this.replaceContent(updated); + this.replaceContent(updated, content.version, 'Content updated successfully.'); }), shareSubscribed(this.dialogs)); } @@ -287,9 +283,7 @@ export abstract class ContentsStateBase extends State { public update(content: ContentDto, request: any): Observable { return this.contentsService.putContent(this.appName, content, request, content.version).pipe( tap(updated => { - this.dialogs.notifyInfo('Content updated successfully.'); - - this.replaceContent(updated, content.version); + this.replaceContent(updated, content.version, 'Content updated successfully.'); }), shareSubscribed(this.dialogs, { silent: true })); } @@ -297,9 +291,7 @@ export abstract class ContentsStateBase extends State { public proposeDraft(content: ContentDto, request: any): Observable { return this.contentsService.proposeDraft(this.appName, content, request, content.version).pipe( tap(updated => { - this.dialogs.notifyInfo('Content updated successfully.'); - - this.replaceContent(updated, content.version); + this.replaceContent(updated, content.version, 'Content updated successfully.'); }), shareSubscribed(this.dialogs, { silent: true })); } @@ -307,9 +299,7 @@ export abstract class ContentsStateBase extends State { public discardDraft(content: ContentDto): Observable { return this.contentsService.discardDraft(this.appName, content, content.version).pipe( tap(updated => { - this.dialogs.notifyInfo('Content updated successfully.'); - - this.replaceContent(updated, content.version); + this.replaceContent(updated, content.version, 'Content updated successfully.'); }), shareSubscribed(this.dialogs)); } @@ -317,9 +307,7 @@ export abstract class ContentsStateBase extends State { public patch(content: ContentDto, request: any): Observable { return this.contentsService.patchContent(this.appName, content, request, content.version).pipe( tap(updated => { - this.dialogs.notifyInfo('Content updated successfully.'); - - this.replaceContent(updated, content.version); + this.replaceContent(updated, content.version, 'Content updated successfully.'); }), shareSubscribed(this.dialogs)); } @@ -344,8 +332,12 @@ export abstract class ContentsStateBase extends State { return this.appsState.appName; } - private replaceContent(content: ContentDto, oldVersion?: Version) { + private replaceContent(content: ContentDto, oldVersion?: Version, updateText?: string) { if (!oldVersion || !oldVersion.eq(content.version)) { + if (updateText) { + this.dialogs.notifyInfo(updateText); + } + return this.next(s => { const contents = s.contents.replaceBy('id', content); diff --git a/frontend/app/shared/state/contributors.forms.ts b/frontend/app/shared/state/contributors.forms.ts index 7f9096161..447079c4d 100644 --- a/frontend/app/shared/state/contributors.forms.ts +++ b/frontend/app/shared/state/contributors.forms.ts @@ -37,7 +37,7 @@ export class AssignContributorForm extends Form })); } - protected transformSubmit(value: { user: string | UserDto, role: string }) { + protected transformSubmit(value: any) { let contributorId = value.user; if (Types.is(contributorId, UserDto)) { @@ -48,7 +48,9 @@ export class AssignContributorForm extends Form } } -export class ImportContributorsForm extends Form> { +type ImportContributorsFormType = ReadonlyArray; + +export class ImportContributorsForm extends Form { public numberOfEmails = value$(this.form.controls['import']).pipe(debounceTime(100), map(v => extractEmails(v).length), shareReplay(1)); public hasNoUser = this.numberOfEmails.pipe(map(v => v === 0)); @@ -63,7 +65,7 @@ export class ImportContributorsForm extends Form { +import { AppLanguageDto, UpdateAppLanguageDto } from './../services/app-languages.service'; + +export class EditLanguageForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ isMaster: false, @@ -34,7 +36,9 @@ export class EditLanguageForm extends Form { +type AddLanguageFormType = { language: LanguageDto }; + +export class AddLanguageForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ language: [null, diff --git a/frontend/app/shared/state/patterns.forms.ts b/frontend/app/shared/state/patterns.forms.ts index a3d571069..60b0ca5d9 100644 --- a/frontend/app/shared/state/patterns.forms.ts +++ b/frontend/app/shared/state/patterns.forms.ts @@ -9,7 +9,9 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Form, ValidatorsEx } from '@app/framework'; -export class EditPatternForm extends Form { +import { EditPatternDto, PatternDto } from './../services/patterns.service'; + +export class EditPatternForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ name: ['', diff --git a/frontend/app/shared/state/roles.forms.ts b/frontend/app/shared/state/roles.forms.ts index fea150306..247df239a 100644 --- a/frontend/app/shared/state/roles.forms.ts +++ b/frontend/app/shared/state/roles.forms.ts @@ -13,7 +13,13 @@ import { hasValue$ } from '@app/framework'; -export class EditPermissionsForm extends Form> { +import { + CreateRoleDto, + RoleDto, + UpdateRoleDto +} from './../services/roles.service'; + +export class EditRoleForm extends Form { public get controls() { return this.form.controls as FormControl[]; } @@ -30,7 +36,13 @@ export class EditPermissionsForm extends Form> this.form.removeAt(index); } - public load(permissions: ReadonlyArray) { + public transformSubmit(value: any) { + return { permissions: value }; + } + + public transformLoad(value: Partial) { + const permissions = value.permissions || []; + while (this.form.controls.length < permissions.length) { this.add(); } @@ -39,11 +51,13 @@ export class EditPermissionsForm extends Form> this.form.removeAt(this.form.controls.length - 1); } - super.load(permissions); + return value.permissions; } } -export class AddPermissionForm extends Form { +type AddPermissionFormType = { permission: string }; + +export class AddPermissionForm extends Form { public hasPermission = hasValue$(this.form.controls['permission']); constructor(formBuilder: FormBuilder) { @@ -57,7 +71,7 @@ export class AddPermissionForm extends Form { } } -export class AddRoleForm extends Form { +export class AddRoleForm extends Form { public hasNoName = hasNoValue$(this.form.controls['name']); constructor(formBuilder: FormBuilder) { diff --git a/frontend/app/shared/state/schemas.forms.ts b/frontend/app/shared/state/schemas.forms.ts index 1d3cf677c..a3269328a 100644 --- a/frontend/app/shared/state/schemas.forms.ts +++ b/frontend/app/shared/state/schemas.forms.ts @@ -10,16 +10,24 @@ import { map } from 'rxjs/operators'; import { Form, - Types, ValidatorsEx, value$ } from '@app/framework'; -import { AddFieldDto } from './../services/schemas.service'; +import { + AddFieldDto, + CreateSchemaDto, + SchemaDetailsDto, + SchemaPropertiesDto, + SynchronizeSchemaDto, + UpdateSchemaDto +} from './../services/schemas.service'; + +import { createProperties, FieldPropertiesDto } from './../services/schemas.types'; -import { createProperties } from './../services/schemas.types'; +type CreateCategoryFormType = { name: string }; -export class CreateCategoryForm extends Form { +export class CreateCategoryForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ name: [''] @@ -27,7 +35,7 @@ export class CreateCategoryForm extends Form { } } -export class CreateSchemaForm extends Form { +export class CreateSchemaForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ name: ['', @@ -41,9 +49,39 @@ export class CreateSchemaForm extends Form { + constructor(formBuilder: FormBuilder) { + super(formBuilder.group({ + json: {}, + fieldsDelete: false, + fieldsRecreate: false + })); + } + + public loadSchema(schema: SchemaDetailsDto) { + this.form.get('json')!.setValue(schema.export()); + } + + public transformSubmit(value: any) { + return { + ...value, + noFieldDeletion: !value.fieldsDelete, + noFieldRecreation: !value.fieldsDelete + }; + } } -export class AddPreviewUrlForm extends Form { +type AddPreviewUrlFormType = { name: string, url: string }; + +export class AddPreviewUrlForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ name: ['', @@ -60,17 +98,9 @@ export class AddPreviewUrlForm extends Form { - constructor(formBuilder: FormBuilder) { - super(formBuilder.group({ - json: {}, - fieldsDelete: false, - fieldsRecreate: false - })); - } -} +type ConfigurePreviewUrlsFormType = { [name: string]: string }; -export class ConfigurePreviewUrlsForm extends Form { +export class ConfigurePreviewUrlsForm extends Form { constructor( private readonly formBuilder: FormBuilder ) { @@ -97,32 +127,32 @@ export class ConfigurePreviewUrlsForm extends Form) { + const result = []; - if (Types.isObject(value)) { - const length = Object.keys(value).length; + const previewUrls = value.previewUrls || {}; - while (this.form.controls.length < length) { - this.add({}); - } + const length = Object.keys(previewUrls).length; - while (this.form.controls.length > length) { - this.remove(this.form.controls.length - 1); - } + while (this.form.controls.length < length) { + this.add({}); + } + + while (this.form.controls.length > length) { + this.remove(this.form.controls.length - 1); + } - for (const key in value) { - if (value.hasOwnProperty(key)) { - result.push({ name: key, url: value[key] }); - } + for (const key in previewUrls) { + if (previewUrls.hasOwnProperty(key)) { + result.push({ name: key, url: previewUrls[key] }); } } return result; } - public transformSubmit(value: ReadonlyArray<{ name: string, url: string }>): { [name: string]: string } { - const result: { [name: string]: string } = {}; + public transformSubmit(value: any) { + const result = {}; for (const item of value) { result[item.name] = item.url; @@ -132,7 +162,7 @@ export class ConfigurePreviewUrlsForm extends Form { +export class EditScriptsForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ query: '', @@ -144,7 +174,7 @@ export class EditScriptsForm extends Form { +export class EditFieldForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ label: ['', @@ -169,7 +199,7 @@ export class EditFieldForm extends Form { +export class EditSchemaForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ label: ['', @@ -208,13 +238,18 @@ export class AddFieldForm extends Form { })); } - public transformLoad(value: AddFieldDto) { + public transformLoad(value: Partial) { const isLocalizable = value.partitioning === 'language'; - return { name: value.name, isLocalizable, type: value.properties.fieldType }; + const type = + value.properties ? + value.properties.fieldType : + 'String'; + + return { name: value.name, isLocalizable, type }; } - public transformSubmit(value: any): AddFieldDto { + public transformSubmit(value: any) { const properties = createProperties(value.type); const partitioning = value.isLocalizable ? 'language' : 'invariant'; diff --git a/frontend/app/shared/state/schemas.state.ts b/frontend/app/shared/state/schemas.state.ts index 75e50c274..38e8d82fb 100644 --- a/frontend/app/shared/state/schemas.state.ts +++ b/frontend/app/shared/state/schemas.state.ts @@ -16,7 +16,8 @@ import { shareMapSubscribed, shareSubscribed, State, - Types + Types, + Version } from '@app/framework'; import { AppsState } from './apps.state'; @@ -246,7 +247,7 @@ export class SchemasState extends State { public configurePreviewUrls(schema: SchemaDto, request: {}): Observable { return this.schemasService.putPreviewUrls(this.appName, schema, request, schema.version).pipe( tap(updated => { - this.replaceSchema(updated); + this.replaceSchema(updated, schema.version, 'Schema saved successfully.'); }), shareSubscribed(this.dialogs)); } @@ -254,7 +255,7 @@ export class SchemasState extends State { public configureScripts(schema: SchemaDto, request: {}): Observable { return this.schemasService.putScripts(this.appName, schema, request, schema.version).pipe( tap(updated => { - this.replaceSchema(updated); + this.replaceSchema(updated, schema.version, 'Schema saved successfully.'); }), shareSubscribed(this.dialogs)); } @@ -262,7 +263,7 @@ export class SchemasState extends State { public synchronize(schema: SchemaDto, request: {}): Observable { return this.schemasService.putSchemaSync(this.appName, schema, request, schema.version).pipe( tap(updated => { - this.replaceSchema(updated); + this.replaceSchema(updated, schema.version, 'Schema synchronized successfully.'); }), shareSubscribed(this.dialogs)); } @@ -270,7 +271,7 @@ export class SchemasState extends State { public update(schema: SchemaDto, request: UpdateSchemaDto): Observable { return this.schemasService.putSchema(this.appName, schema, request, schema.version).pipe( tap(updated => { - this.replaceSchema(updated); + this.replaceSchema(updated, schema.version, 'Schema saved successfully.'); }), shareSubscribed(this.dialogs)); } @@ -286,7 +287,7 @@ export class SchemasState extends State { public configureUIFields(schema: SchemaDto, request: UpdateUIFields): Observable { return this.schemasService.putUIFields(this.appName, schema, request, schema.version).pipe( tap(updated => { - this.replaceSchema(updated); + this.replaceSchema(updated, schema.version, 'Schema saved successfully.'); }), shareSubscribed(this.dialogs)); } @@ -294,7 +295,7 @@ export class SchemasState extends State { public orderFields(schema: SchemaDto, fields: ReadonlyArray, parent?: RootFieldDto): Observable { return this.schemasService.putFieldOrdering(this.appName, parent || schema, fields.map(t => t.fieldId), schema.version).pipe( tap(updated => { - this.replaceSchema(updated); + this.replaceSchema(updated, schema.version, 'Schema saved successfully.'); }), shareSubscribed(this.dialogs)); } @@ -342,7 +343,7 @@ export class SchemasState extends State { public updateField(schema: SchemaDto, field: T, request: UpdateFieldDto): Observable { return this.schemasService.putField(this.appName, field, request, schema.version).pipe( tap(updated => { - this.replaceSchema(updated); + this.replaceSchema(updated, schema.version, 'Schema saved successfully.'); }), shareSubscribed(this.dialogs)); } @@ -355,20 +356,30 @@ export class SchemasState extends State { shareSubscribed(this.dialogs)); } - private replaceSchema(schema: SchemaDto) { - return this.next(s => { - const schemas = s.schemas.replaceBy('id', schema).sortedByString(x => x.displayName); - - const selectedSchema = - Types.is(schema, SchemaDetailsDto) && - schema && - s.selectedSchema && - s.selectedSchema.id === schema.id ? - schema : - s.selectedSchema; + private replaceSchema(schema: SchemaDto, oldVersion?: Version, updateText?: string) { + if (!oldVersion || !oldVersion.eq(schema.version)) { + if (updateText) { + this.dialogs.notifyInfo(updateText); + } - return { ...s, schemas, selectedSchema }; - }); + this.next(s => { + const schemas = s.schemas.replaceBy('id', schema).sortedByString(x => x.displayName); + + const selectedSchema = + Types.is(schema, SchemaDetailsDto) && + schema && + s.selectedSchema && + s.selectedSchema.id === schema.id ? + schema : + s.selectedSchema; + + return { ...s, schemas, selectedSchema }; + }); + } else { + if (updateText) { + this.dialogs.notifyInfo('Nothing has been changed.'); + } + } } private get appName() { diff --git a/frontend/app/shared/state/workflows.forms.ts b/frontend/app/shared/state/workflows.forms.ts index 7c0fc21ac..e212d035e 100644 --- a/frontend/app/shared/state/workflows.forms.ts +++ b/frontend/app/shared/state/workflows.forms.ts @@ -9,7 +9,9 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Form, hasNoValue$ } from '@app/framework'; -export class AddWorkflowForm extends Form { +import { CreateWorkflowDto } from './../services/workflows.service'; + +export class AddWorkflowForm extends Form { public hasNoName = hasNoValue$(this.form.controls['name']); constructor(formBuilder: FormBuilder) { diff --git a/frontend/app/theme/_bootstrap.scss b/frontend/app/theme/_bootstrap.scss index 378e2978c..45faf85b6 100644 --- a/frontend/app/theme/_bootstrap.scss +++ b/frontend/app/theme/_bootstrap.scss @@ -593,7 +593,7 @@ a { } &-secondary2 { - @include build-text-button(#777c7b); + @include build-text-button($color-text); } } }