diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs index eafa95d22..8f2f2689c 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs @@ -34,11 +34,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { var typedValue = value; - if (value == null) - { - typedValue = Undefined.Value; - } - else if (value is IJsonValue jsonValue) + if (value is IJsonValue jsonValue) { if (jsonValue.Type == JsonValueType.Null) { diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs index 6c2b26246..c86c85be3 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public async Task ValidateAsync(object value, ValidationContext context, AddError addError) { - if (value == null) + if (value.IsNullOrUndefined()) { value = DefaultValue; } @@ -49,17 +49,22 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { var name = field.Key; - if (!values.TryGetValue(name, out var fieldValue)) + var (isOptional, validator) = field.Value; + + var fieldValue = Undefined.Value; + + if (!values.TryGetValue(name, out var temp)) { if (isPartial) { continue; } - - fieldValue = default; + } + else + { + fieldValue = temp; } - var (isOptional, validator) = field.Value; var fieldContext = context.Nested(name).Optional(isOptional); tasks.Add(validator.ValidateAsync(fieldValue, fieldContext, addError)); diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs new file mode 100644 index 000000000..8f94a80cf --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public static class AppExtensions + { + public static NamedId NamedId(this IAppEntity app) + { + return new NamedId(app.Id, app.Name); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs index 3645192a0..63ac53ab9 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs @@ -194,7 +194,7 @@ namespace Squidex.Domain.Apps.Entities.Apps } else { - var result = await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.Id, Snapshot.Name, c.PlanId); + var result = await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), c.PlanId); switch (result) { @@ -213,7 +213,7 @@ namespace Squidex.Domain.Apps.Entities.Apps case ArchiveApp archiveApp: return UpdateAsync(archiveApp, async c => { - await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.Id, Snapshot.Name, null); + await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), null); ArchiveApp(c); }); diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs index 89c6342cd..933a11ddf 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs @@ -7,6 +7,7 @@ using System; using System.Threading.Tasks; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Services { @@ -14,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services { bool HasPortal { get; } - Task ChangePlanAsync(string userId, Guid appId, string appName, string planId); + Task ChangePlanAsync(string userId, NamedId appId, string planId); Task GetPortalLinkAsync(string userId); } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs index 8e968ccc7..b8c1f46ef 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs @@ -7,6 +7,7 @@ using System; using System.Threading.Tasks; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations { @@ -17,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations get { return false; } } - public Task ChangePlanAsync(string userId, Guid appId, string appName, string planId) + public Task ChangePlanAsync(string userId, NamedId appId, string planId) { return Task.FromResult(new PlanResetResult()); } diff --git a/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs b/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs new file mode 100644 index 000000000..b5c78cd43 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public static class EntityExtensions + { + public static NamedId NamedId(this IAppEntity entity) + { + return new NamedId(entity.Id, entity.Name); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs index ab4851731..4e937c96e 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs @@ -142,49 +142,73 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards fieldIndex++; fieldPrefix = $"Fields[{fieldIndex}]"; - if (!field.Partitioning.IsValidPartitioning()) - { - e(Not.Valid("Partitioning"), $"{fieldPrefix}.{nameof(field.Partitioning)}"); - } + ValidateRootField(field, fieldPrefix, e); + } - ValidateField(field, fieldPrefix, e); + if (command.Fields.Select(x => x?.Name).Distinct().Count() != command.Fields.Count) + { + e("Fields cannot have duplicate names.", nameof(command.Fields)); + } + } + } - if (field.Nested?.Count > 0) - { - if (field.Properties is ArrayFieldProperties) - { - var nestedIndex = 0; - var nestedPrefix = string.Empty; + private static void ValidateRootField(UpsertSchemaField field, string prefix, AddValidation e) + { + if (field == null) + { + e(Not.Defined("Field"), prefix); + } + else + { + if (!field.Partitioning.IsValidPartitioning()) + { + e(Not.Valid("Partitioning"), $"{prefix}.{nameof(field.Partitioning)}"); + } - foreach (var nestedField in field.Nested) - { - nestedIndex++; - nestedPrefix = $"{fieldPrefix}.Nested[{nestedIndex}]"; + ValidateField(field, prefix, e); - if (nestedField.Properties is ArrayFieldProperties) - { - e("Nested field cannot be array fields.", $"{nestedPrefix}.{nameof(nestedField.Properties)}"); - } + if (field.Nested?.Count > 0) + { + if (field.Properties is ArrayFieldProperties) + { + var nestedIndex = 0; + var nestedPrefix = string.Empty; - ValidateField(nestedField, nestedPrefix, e); - } - } - else if (field.Nested.Count > 0) + foreach (var nestedField in field.Nested) { - e("Only array fields can have nested fields.", $"{fieldPrefix}.{nameof(field.Partitioning)}"); - } + nestedIndex++; + nestedPrefix = $"{prefix}.Nested[{nestedIndex}]"; - if (field.Nested.Select(x => x.Name).Distinct().Count() != field.Nested.Count) - { - e("Fields cannot have duplicate names.", $"{fieldPrefix}.Nested"); + ValidateNestedField(nestedField, nestedPrefix, e); } } + else if (field.Nested.Count > 0) + { + e("Only array fields can have nested fields.", $"{prefix}.{nameof(field.Partitioning)}"); + } + + if (field.Nested.Select(x => x.Name).Distinct().Count() != field.Nested.Count) + { + e("Fields cannot have duplicate names.", $"{prefix}.Nested"); + } } + } + } - if (command.Fields.Select(x => x.Name).Distinct().Count() != command.Fields.Count) + private static void ValidateNestedField(UpsertSchemaNestedField nestedField, string prefix, AddValidation e) + { + if (nestedField == null) + { + e(Not.Defined("Field"), prefix); + } + else + { + if (nestedField.Properties is ArrayFieldProperties) { - e("Fields cannot have duplicate names.", nameof(command.Fields)); + e("Nested field cannot be array fields.", $"{prefix}.{nameof(nestedField.Properties)}"); } + + ValidateField(nestedField, prefix, e); } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs index 5905b66fd..44e96fa7f 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs @@ -321,7 +321,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas { if (id.HasValue && Snapshot.SchemaDef.FieldsById.TryGetValue(id.Value, out var field)) { - return NamedId.Of(field.Id, field.Name); + return field.NamedId(); } return null; @@ -333,13 +333,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas { if (Snapshot.SchemaDef.FieldsById.TryGetValue(pc.ParentFieldId.Value, out var field)) { - pe.ParentFieldId = NamedId.Of(field.Id, field.Name); + pe.ParentFieldId = field.NamedId(); if (command is FieldCommand fc && @event is FieldEvent fe) { if (field is IArrayField arrayField && arrayField.FieldsById.TryGetValue(fc.FieldId, out var nestedField)) { - fe.FieldId = NamedId.Of(nestedField.Id, nestedField.Name); + fe.FieldId = nestedField.NamedId(); } } } @@ -357,7 +357,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas { if (@event.SchemaId == null) { - @event.SchemaId = NamedId.Of(Snapshot.Id, Snapshot.SchemaDef.Name); + @event.SchemaId = Snapshot.NamedId(); } if (@event.AppId == null) diff --git a/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs index 06e050784..0b7d872e5 100644 --- a/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs +++ b/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs @@ -57,7 +57,7 @@ namespace Squidex.Web.CommandMiddlewares throw new InvalidOperationException("Cannot resolve app."); } - return NamedId.Of(appFeature.App.Id, appFeature.App.Name); + return appFeature.App.NamedId(); } } } \ No newline at end of file diff --git a/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs index 672a16b74..a64798783 100644 --- a/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs +++ b/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs @@ -69,7 +69,7 @@ namespace Squidex.Web.CommandMiddlewares if (appFeature?.App != null) { - appId = NamedId.Of(appFeature.App.Id, appFeature.App.Name); + appId = appFeature.App.NamedId(); } } diff --git a/src/Squidex/app/shared/state/contents.forms.ts b/src/Squidex/app/shared/state/contents.forms.ts index d37732d70..b5c1e1134 100644 --- a/src/Squidex/app/shared/state/contents.forms.ts +++ b/src/Squidex/app/shared/state/contents.forms.ts @@ -331,7 +331,7 @@ export class EditContentForm extends Form { super(new FormGroup({})); for (const field of schema.fields) { - if (field.properties.fieldType !== 'UI') { + if (field.properties.isContentField) { const fieldForm = new FormGroup({}); const fieldDefault = FieldDefaultValue.get(field); @@ -378,7 +378,7 @@ export class EditContentForm extends Form { let isOptional = field.isLocalizable && !!language && language.isOptional; for (let nested of field.nested) { - if (nested.properties.fieldType !== 'UI') { + if (nested.properties.isContentField) { const nestedValidators = FieldValidatorsFactory.createValidators(nested, isOptional); let value = FieldDefaultValue.get(nested); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index 496fc96ca..665762927 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -339,9 +339,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_array_field_has_required_nested_field() { - schema = - schema.AddArray(1, "my-field", Partitioning.Invariant, f => f. - AddNumber(1, "my-nested", new NumberFieldProperties { IsRequired = true })); + schema = schema.AddArray(1, "my-field", Partitioning.Invariant, f => f. + AddNumber(2, "my-nested", new NumberFieldProperties { IsRequired = true })); var data = new NamedContentData() @@ -362,5 +361,37 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new ValidationError("Field is required.", "my-field[3].my-nested") }); } + + [Fact] + public async Task Should_not_add_error_if_separator_not_defined() + { + schema = schema.AddUI(2, "ui", Partitioning.Invariant); + + var data = + new NamedContentData(); + + await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_nested_separator_not_defined() + { + schema = schema.AddArray(1, "my-field", Partitioning.Invariant, f => f. + AddUI(2, "my-nested")); + + var data = + new NamedContentData() + .AddField("my-field", + new ContentFieldData() + .AddValue("iv", + JsonValue.Array( + JsonValue.Object()))); + + await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + + Assert.Empty(errors); + } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs index 1ac33f4b1..cf6a405df 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs @@ -103,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { var command = new ChangePlan { PlanId = planIdPaid }; - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, planIdPaid)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppNamedId, planIdPaid)) .Returns(new PlanChangedResult()); await ExecuteCreateAsync(); @@ -125,10 +125,10 @@ namespace Squidex.Domain.Apps.Entities.Apps { var command = new ChangePlan { PlanId = planIdFree }; - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, planIdPaid)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppNamedId, planIdPaid)) .Returns(new PlanChangedResult()); - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, planIdFree)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppNamedId, planIdFree)) .Returns(new PlanResetResult()); await ExecuteCreateAsync(); @@ -151,7 +151,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { var command = new ChangePlan { PlanId = planIdPaid }; - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, planIdPaid)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppNamedId, planIdPaid)) .Returns(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); await ExecuteCreateAsync(); @@ -174,7 +174,7 @@ namespace Squidex.Domain.Apps.Entities.Apps result.ShouldBeEquivalent(new EntitySavedResult(5)); - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, planIdPaid)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppNamedId, planIdPaid)) .MustNotHaveHappened(); } @@ -486,7 +486,7 @@ namespace Squidex.Domain.Apps.Entities.Apps CreateEvent(new AppArchived()) ); - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, AppId, AppName, null)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, AppNamedId, null)) .MustHaveHappened(); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs index 39172ada7..547db7299 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; using Xunit; @@ -25,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Billing [Fact] public async Task Should_do_nothing_when_changing_plan() { - await sut.ChangePlanAsync(null, Guid.Empty, null, null); + await sut.ChangePlanAsync(null, null, null); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs index 815c45dab..15e5b1cd6 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs @@ -174,7 +174,7 @@ namespace Squidex.Domain.Apps.Entities.History.Notifications var @event = new AppContributorAssigned { Actor = new RefToken(assignerType, assignerId), - AppId = new NamedId(Guid.NewGuid(), appName), + AppId = NamedId.Of(Guid.NewGuid(), appName), ContributorId = assigneeId, IsCreated = isNewUser, IsAdded = isNewContributor, diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs index 16398cc53..5c4ff17fe 100644 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs +++ b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs @@ -81,7 +81,7 @@ namespace Squidex.Web.CommandMiddlewares [Fact] public async Task Should_assign_app_id_to_app_self_command() { - var command = new AddPattern(); + var command = new ChangePlan(); var context = new CommandContext(command, commandBus); await sut.HandleAsync(context); @@ -92,7 +92,7 @@ namespace Squidex.Web.CommandMiddlewares [Fact] public async Task Should_not_override_app_id() { - var command = new AddPattern { AppId = Guid.NewGuid() }; + var command = new ChangePlan { AppId = Guid.NewGuid() }; var context = new CommandContext(command, commandBus); await sut.HandleAsync(context);