Browse Source

Guards simplified.

pull/303/head
Sebastian 8 years ago
parent
commit
03786ba2c0
  1. 35
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs
  2. 42
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
  3. 59
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  4. 42
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs
  5. 26
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPattern.cs
  6. 6
      src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs
  7. 39
      src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  8. 18
      src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs
  9. 19
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs
  10. 24
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaFieldBase.cs
  11. 9
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaNestedField.cs
  12. 47
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs
  13. 132
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  14. 71
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs
  15. 17
      src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs
  16. 24
      src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs
  17. 11
      src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs
  18. 38
      src/Squidex.Infrastructure/Validate.cs
  19. 5
      src/Squidex.Infrastructure/ValidationError.cs
  20. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs
  21. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs
  22. 56
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs

35
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs

@ -20,15 +20,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
Guard.NotNull(command, nameof(command));
return Validate.It(() => "Cannot create app.", async error =>
return Validate.It(() => "Cannot create app.", async e =>
{
if (!command.Name.IsSlug())
{
error(new ValidationError("Name must be a valid slug.", nameof(command.Name)));
e("Name must be a valid slug.", nameof(command.Name));
}
else if (await appProvider.GetAppAsync(command.Name) != null)
{
error(new ValidationError($"An app with the same name already exists.", nameof(command.Name)));
e("An app with the same name already exists.", nameof(command.Name));
}
});
}
@ -37,28 +37,27 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot change plan.", error =>
Validate.It(() => "Cannot change plan.", e =>
{
if (string.IsNullOrWhiteSpace(command.PlanId))
{
error(new ValidationError("Plan id is required.", nameof(command.PlanId)));
e("Plan id is required.", nameof(command.PlanId));
return;
}
else
if (appPlans.GetPlan(command.PlanId) == null)
{
if (appPlans.GetPlan(command.PlanId) == null)
{
error(new ValidationError("A plan with this id does not exist.", nameof(command.PlanId)));
}
e("A plan with this id does not exist.", nameof(command.PlanId));
}
if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor))
{
error(new ValidationError("Plan can only changed from the user who configured the plan initially."));
}
if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor))
{
e("Plan can only changed from the user who configured the plan initially.");
}
if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase))
{
error(new ValidationError("App has already this plan."));
}
if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase))
{
e("App has already this plan.");
}
});
}

42
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs

@ -17,15 +17,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot attach client.", error =>
Validate.It(() => "Cannot attach client.", e =>
{
if (string.IsNullOrWhiteSpace(command.Id))
{
error(new ValidationError("Client id is required.", nameof(command.Id)));
e("Client id is required.", nameof(command.Id));
}
else if (clients.ContainsKey(command.Id))
{
error(new ValidationError($"A client with the same id already exists."));
e($"A client with the same id already exists.");
}
});
}
@ -36,11 +36,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
GetClientOrThrow(clients, command.Id);
Validate.It(() => "Cannot revoke client.", error =>
Validate.It(() => "Cannot revoke client.", e =>
{
if (string.IsNullOrWhiteSpace(command.Id))
{
error(new ValidationError("Client id is required.", nameof(command.Id)));
e("Client id is required.", nameof(command.Id));
}
});
}
@ -51,41 +51,43 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var client = GetClientOrThrow(clients, command.Id);
Validate.It(() => "Cannot revoke client.", error =>
Validate.It(() => "Cannot update client.", e =>
{
if (string.IsNullOrWhiteSpace(command.Id))
{
error(new ValidationError("Client id is required.", nameof(command.Id)));
e("Client id is required.", nameof(command.Id));
}
if (string.IsNullOrWhiteSpace(command.Name) && command.Permission == null)
{
error(new ValidationError("Either name or permission must be defined.", nameof(command.Name), nameof(command.Permission)));
e("Either name or permission must be defined.", nameof(command.Name), nameof(command.Permission));
}
if (command.Permission.HasValue && !command.Permission.Value.IsEnumValue())
{
error(new ValidationError("Permission is not valid.", nameof(command.Permission)));
e("Permission is not valid.", nameof(command.Permission));
}
if (client != null)
if (client == null)
{
if (!string.IsNullOrWhiteSpace(command.Name) && string.Equals(client.Name, command.Name))
{
error(new ValidationError("Client has already this name.", nameof(command.Name)));
}
if (command.Permission == client.Permission)
{
error(new ValidationError("Client has already this permission.", nameof(command.Permission)));
}
return;
}
if (!string.IsNullOrWhiteSpace(command.Name) && string.Equals(client.Name, command.Name))
{
e("Client has already this name.", nameof(command.Name));
}
if (command.Permission == client.Permission)
{
e("Client has already this permission.", nameof(command.Permission));
}
});
}
private static AppClient GetClientOrThrow(AppClients clients, string id)
{
if (id == null)
if (string.IsNullOrWhiteSpace(id))
{
return null;
}

59
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -7,6 +7,7 @@
using System;
using System.Linq;
using System.Security;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
@ -22,46 +23,44 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
Guard.NotNull(command, nameof(command));
return Validate.It(() => "Cannot assign contributor.", async error =>
return Validate.It(() => "Cannot assign contributor.", async e =>
{
if (!command.Permission.IsEnumValue())
{
error(new ValidationError("Permission is not valid.", nameof(command.Permission)));
e("Permission is not valid.", nameof(command.Permission));
}
if (string.IsNullOrWhiteSpace(command.ContributorId))
{
error(new ValidationError("Contributor id is required.", nameof(command.ContributorId)));
e("Contributor id is required.", nameof(command.ContributorId));
return;
}
else
var user = await users.FindByIdOrEmailAsync(command.ContributorId);
if (user == null)
{
var user = await users.FindByIdOrEmailAsync(command.ContributorId);
throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity));
}
if (user == null)
{
throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity));
}
else
{
command.ContributorId = user.Id;
command.ContributorId = user.Id;
if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase))
{
error(new ValidationError("You cannot change your own permission."));
}
else if (contributors.TryGetValue(command.ContributorId, out var existing))
{
if (existing == command.Permission)
{
error(new ValidationError("Contributor has already this permission.", nameof(command.Permission)));
}
}
else if (plan.MaxContributors == contributors.Count)
{
error(new ValidationError("You have reached the maximum number of contributors for your plan."));
}
if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase))
{
throw new SecurityException("You cannot change your own permission.");
}
if (contributors.TryGetValue(command.ContributorId, out var existing))
{
if (existing == command.Permission)
{
e("Contributor has already this permission.", nameof(command.Permission));
}
}
else if (plan.MaxContributors == contributors.Count)
{
e("You have reached the maximum number of contributors for your plan.");
}
});
}
@ -69,18 +68,18 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot remove contributor.", error =>
Validate.It(() => "Cannot remove contributor.", e =>
{
if (string.IsNullOrWhiteSpace(command.ContributorId))
{
error(new ValidationError("Contributor id is required.", nameof(command.ContributorId)));
e("Contributor id is required.", nameof(command.ContributorId));
}
var ownerIds = contributors.Where(x => x.Value == AppContributorPermission.Owner).Select(x => x.Key).ToList();
if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId))
{
error(new ValidationError("Cannot remove the only owner."));
e("Cannot remove the only owner.");
}
});

42
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs

@ -17,15 +17,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot add language.", error =>
Validate.It(() => "Cannot add language.", e =>
{
if (command.Language == null)
{
error(new ValidationError("Language code is required.", nameof(command.Language)));
e("Language code is required.", nameof(command.Language));
}
else if (languages.Contains(command.Language))
{
error(new ValidationError("Language has already been added."));
e("Language has already been added.");
}
});
}
@ -34,18 +34,18 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
Guard.NotNull(command, nameof(command));
var languageConfig = GetLanguageConfigOrThrow(languages, command.Language);
var config = GetConfigOrThrow(languages, command.Language);
Validate.It(() => "Cannot remove language.", error =>
Validate.It(() => "Cannot remove language.", e =>
{
if (command.Language == null)
{
error(new ValidationError("Language code is required.", nameof(command.Language)));
e("Language code is required.", nameof(command.Language));
}
if (languages.Master == languageConfig)
if (languages.Master == config)
{
error(new ValidationError("Master language cannot be removed."));
e("Master language cannot be removed.");
}
});
}
@ -54,34 +54,36 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
Guard.NotNull(command, nameof(command));
var languageConfig = GetLanguageConfigOrThrow(languages, command.Language);
var config = GetConfigOrThrow(languages, command.Language);
Validate.It(() => "Cannot update language.", error =>
Validate.It(() => "Cannot update language.", e =>
{
if (command.Language == null)
{
error(new ValidationError("Language is required.", nameof(command.Language)));
e("Language is required.", nameof(command.Language));
}
if ((languages.Master == languageConfig || command.IsMaster) && command.IsOptional)
if ((languages.Master == config || command.IsMaster) && command.IsOptional)
{
error(new ValidationError("Master language cannot be made optional.", nameof(command.IsMaster)));
e("Master language cannot be made optional.", nameof(command.IsMaster));
}
if (command.Fallback != null)
if (command.Fallback == null)
{
foreach (var fallback in command.Fallback)
return;
}
foreach (var fallback in command.Fallback)
{
if (!languages.Contains(fallback))
{
if (!languages.Contains(fallback))
{
error(new ValidationError($"App does not have fallback language '{fallback}'.", nameof(command.Fallback)));
}
e($"App does not have fallback language '{fallback}'.", nameof(command.Fallback));
}
}
});
}
private static LanguageConfig GetLanguageConfigOrThrow(LanguagesConfig languages, Language language)
private static LanguageConfig GetConfigOrThrow(LanguagesConfig languages, Language language)
{
if (language == null)
{

26
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPattern.cs

@ -18,35 +18,35 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot add pattern.", error =>
Validate.It(() => "Cannot add pattern.", e =>
{
if (command.PatternId == Guid.Empty)
{
error(new ValidationError("Id is required.", nameof(command.PatternId)));
e("Id is required.", nameof(command.PatternId));
}
if (string.IsNullOrWhiteSpace(command.Name))
{
error(new ValidationError("Name is required.", nameof(command.Name)));
e("Name is required.", nameof(command.Name));
}
if (patterns.Values.Any(x => x.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase)))
{
error(new ValidationError("An pattern with the same name already exists."));
e("A pattern with the same name already exists.");
}
if (string.IsNullOrWhiteSpace(command.Pattern))
{
error(new ValidationError("Pattern is required.", nameof(command.Pattern)));
e("Pattern is required.", nameof(command.Pattern));
}
else if (!command.Pattern.IsValidRegex())
{
error(new ValidationError("Pattern is not a valid regular expression.", nameof(command.Pattern)));
e("Pattern is not a valid regular expression.", nameof(command.Pattern));
}
if (patterns.Values.Any(x => x.Pattern == command.Pattern))
{
error(new ValidationError("This pattern already exists but with another name."));
e("This pattern already exists but with another name.");
}
});
}
@ -70,30 +70,30 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern));
}
Validate.It(() => "Cannot update pattern.", error =>
Validate.It(() => "Cannot update pattern.", e =>
{
if (string.IsNullOrWhiteSpace(command.Name))
{
error(new ValidationError("Name is required.", nameof(command.Name)));
e("Name is required.", nameof(command.Name));
}
if (patterns.Any(x => x.Key != command.PatternId && x.Value.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase)))
{
error(new ValidationError("An pattern with the same name already exists."));
e("A pattern with the same name already exists.");
}
if (string.IsNullOrWhiteSpace(command.Pattern))
{
error(new ValidationError("Pattern is required.", nameof(command.Pattern)));
e("Pattern is required.", nameof(command.Pattern));
}
else if (!command.Pattern.IsValidRegex())
{
error(new ValidationError("Pattern is not a valid regular expression.", nameof(command.Pattern)));
e("Pattern is not a valid regular expression.", nameof(command.Pattern));
}
if (patterns.Any(x => x.Key != command.PatternId && x.Value.Pattern == command.Pattern))
{
error(new ValidationError("This pattern already exists but with another name."));
e("This pattern already exists but with another name.");
}
});
}

6
src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs

@ -16,16 +16,16 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot rename asset.", error =>
Validate.It(() => "Cannot rename asset.", e =>
{
if (string.IsNullOrWhiteSpace(command.FileName))
{
error(new ValidationError("Name is required.", nameof(command.FileName)));
e("Name is required.", nameof(command.FileName));
}
if (string.Equals(command.FileName, oldName))
{
error(new ValidationError("Asset has already this name.", nameof(command.FileName)));
e("Asset has already this name.", nameof(command.FileName));
}
});
}

39
src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs

@ -18,12 +18,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot created content.", error =>
Validate.It(() => "Cannot created content.", e =>
{
if (command.Data == null)
{
error(new ValidationError("Data cannot be null.", nameof(command.Data)));
}
ValidateData(command, e);
});
}
@ -31,12 +28,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot update content.", error =>
Validate.It(() => "Cannot update content.", e =>
{
if (command.Data == null)
{
error(new ValidationError("Data cannot be null.", nameof(command.Data)));
}
ValidateData(command, e);
});
}
@ -44,12 +38,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot patch content.", error =>
Validate.It(() => "Cannot patch content.", e =>
{
if (command.Data == null)
{
error(new ValidationError("Data cannot be null.", nameof(command.Data)));
}
ValidateData(command, e);
});
}
@ -57,11 +48,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot discard pending changes.", error =>
Validate.It(() => "Cannot discard pending changes.", e =>
{
if (!isPending)
{
error(new ValidationError("The content has no pending changes."));
e("The content has no pending changes.");
}
});
}
@ -70,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot change status.", error =>
Validate.It(() => "Cannot change status.", e =>
{
var isAllowedPendingUpdate =
status == command.Status &&
@ -79,12 +70,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
if (!StatusFlow.Exists(command.Status) || (!StatusFlow.CanChange(status, command.Status) && !isAllowedPendingUpdate))
{
error(new ValidationError($"Content cannot be changed from status {status} to {command.Status}.", nameof(command.Status)));
e($"Content cannot be changed from status {status} to {command.Status}.", nameof(command.Status));
}
if (command.DueTime.HasValue && command.DueTime.Value < SystemClock.Instance.GetCurrentInstant())
{
error(new ValidationError("DueTime must be in the future.", nameof(command.DueTime)));
e("DueTime must be in the future.", nameof(command.DueTime));
}
});
}
@ -93,5 +84,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
Guard.NotNull(command, nameof(command));
}
private static void ValidateData(ContentDataCommand command, AddValidation e)
{
if (command.Data == null)
{
e("Data is required.", nameof(command.Data));
}
}
}
}

18
src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs

@ -19,28 +19,28 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
{
Guard.NotNull(command, nameof(command));
return Validate.It(() => "Cannot create rule.", async error =>
return Validate.It(() => "Cannot create rule.", async e =>
{
if (command.Trigger == null)
{
error(new ValidationError("Trigger is required.", nameof(command.Trigger)));
e("Trigger is required.", nameof(command.Trigger));
}
else
{
var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider);
errors.Foreach(error);
errors.Foreach(x => x.AddTo(e));
}
if (command.Action == null)
{
error(new ValidationError("Action is required.", nameof(command.Action)));
e("Action is required.", nameof(command.Action));
}
else
{
var errors = await RuleActionValidator.ValidateAsync(command.Action);
errors.Foreach(error);
errors.Foreach(x => x.AddTo(e));
}
});
}
@ -49,25 +49,25 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
{
Guard.NotNull(command, nameof(command));
return Validate.It(() => "Cannot update rule.", async error =>
return Validate.It(() => "Cannot update rule.", async e =>
{
if (command.Trigger == null && command.Action == null)
{
error(new ValidationError("Either trigger or action is required.", nameof(command.Trigger), nameof(command.Action)));
e("Either trigger or action is required.", nameof(command.Trigger), nameof(command.Action));
}
if (command.Trigger != null)
{
var errors = await RuleTriggerValidator.ValidateAsync(appId, command.Trigger, appProvider);
errors.Foreach(error);
errors.Foreach(x => x.AddTo(e));
}
if (command.Action != null)
{
var errors = await RuleActionValidator.ValidateAsync(command.Action);
errors.Foreach(error);
errors.Foreach(x => x.AddTo(e));
}
});
}

19
src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs

@ -5,25 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using FieldNested = System.Collections.Generic.List<Squidex.Domain.Apps.Entities.Schemas.Commands.CreateSchemaNestedField>;
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class CreateSchemaField
public sealed class CreateSchemaField : CreateSchemaFieldBase
{
public string Partitioning { get; set; } = Core.Partitioning.Invariant.Key;
public string Partitioning { get; set; } = "invariant";
public string Name { get; set; }
public bool IsHidden { get; set; }
public bool IsLocked { get; set; }
public bool IsDisabled { get; set; }
public FieldNested Nested { get; set; }
public FieldProperties Properties { get; set; }
public List<CreateSchemaNestedField> Nested { get; set; }
}
}

24
src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaFieldBase.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public abstract class CreateSchemaFieldBase
{
public string Name { get; set; }
public bool IsLocked { get; set; }
public bool IsHidden { get; set; }
public bool IsDisabled { get; set; }
public FieldProperties Properties { get; set; }
}
}

9
src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaNestedField.cs

@ -9,14 +9,7 @@ using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class CreateSchemaNestedField
public sealed class CreateSchemaNestedField : CreateSchemaFieldBase
{
public string Name { get; set; }
public bool IsHidden { get; set; }
public bool IsDisabled { get; set; }
public FieldProperties Properties { get; set; }
}
}

47
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs

@ -0,0 +1,47 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
public static class GuardHelper
{
public static IArrayField GetArrayFieldOrThrow(Schema schema, long parentId)
{
if (!schema.FieldsById.TryGetValue(parentId, out var rootField) || !(rootField is IArrayField arrayField))
{
throw new DomainObjectNotFoundException(parentId.ToString(), "Fields", typeof(Schema));
}
return arrayField;
}
public static IField GetFieldOrThrow(Schema schema, long fieldId, long? parentId)
{
if (parentId.HasValue)
{
var arrayField = GetArrayFieldOrThrow(schema, parentId.Value);
if (!arrayField.FieldsById.TryGetValue(fieldId, out var nestedField))
{
throw new DomainObjectNotFoundException(fieldId.ToString(), $"Fields[{parentId}].Fields", typeof(Schema));
}
return nestedField;
}
if (!schema.FieldsById.TryGetValue(fieldId, out var field))
{
throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Schema));
}
return field;
}
}
}

132
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -22,106 +21,69 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
return Validate.It(() => "Cannot create schema.", async error =>
return Validate.It(() => "Cannot create schema.", async e =>
{
if (!command.Name.IsSlug())
{
error(new ValidationError("Name is not a valid slug.", nameof(command.Name)));
e("Name is not a valid slug.", nameof(command.Name));
}
else if (await appProvider.GetSchemaAsync(command.AppId.Id, command.Name) != null)
{
error(new ValidationError("A schema with the same name already exists."));
e("A schema with the same name already exists.");
}
if (command.Fields?.Count > 0)
{
var index = 0;
var fieldIndex = 0;
var fieldPrefix = string.Empty;
foreach (var field in command.Fields)
{
index++;
var prefix = $"Fields[{index}]";
fieldIndex++;
fieldPrefix = $"Fields[{fieldIndex}]";
if (!field.Partitioning.IsValidPartitioning())
{
error(new ValidationError("Field partitioning is not valid.",
$"{prefix}.{nameof(field.Partitioning)}"));
}
if (!field.Name.IsPropertyName())
{
error(new ValidationError("Field name must be a valid javascript property name.",
$"{prefix}.{nameof(field.Name)}"));
e("Field partitioning is not valid.", $"{fieldPrefix}.{nameof(field.Partitioning)}");
}
if (field.Properties == null)
{
error(new ValidationError("Field properties is required.",
$"{prefix}.{nameof(field.Properties)}"));
}
else
{
var errors = FieldPropertiesValidator.Validate(field.Properties);
foreach (var e in errors)
{
error(e.WithPrefix($"{prefix}.Properties"));
}
}
ValidateField(e, fieldPrefix, field);
if (field.Nested?.Count > 0)
{
if (!(field.Properties is ArrayFieldProperties))
{
error(new ValidationError("Only array fields can have nested fields.",
$"{prefix}.{nameof(field.Partitioning)}"));
}
else
if (field.Properties is ArrayFieldProperties)
{
var nestedIndex = 0;
var nestedPrefix = string.Empty;
foreach (var nestedField in field.Nested)
{
nestedIndex++;
nestedPrefix = $"{fieldPrefix}.Nested[{nestedIndex}]";
var nestedPrefix = $"{prefix}.Nested[{nestedIndex}]";
if (!nestedField.Name.IsPropertyName())
{
error(new ValidationError("Nested field name must be a valid javascript property name.",
$"{nestedPrefix}.{nameof(nestedField.Name)}"));
}
if (nestedField.Properties == null)
if (nestedField.Properties is ArrayFieldProperties)
{
error(new ValidationError("Nested field properties is required.",
$"{nestedPrefix}.{nameof(nestedField.Properties)}"));
e("Nested field cannot be array fields.", $"{nestedPrefix}.{nameof(nestedField.Properties)}");
}
else
{
var errors = FieldPropertiesValidator.Validate(nestedField.Properties);
foreach (var e in errors)
{
error(e.WithPrefix($"{nestedPrefix}.Properties"));
}
}
ValidateField(e, nestedPrefix, nestedField);
}
}
else if (field.Nested.Count > 0)
{
e("Only array fields can have nested fields.", $"{fieldPrefix}.{nameof(field.Partitioning)}");
}
if (field.Nested.Select(x => x.Name).Distinct().Count() != field.Nested.Count)
{
error(new ValidationError("Fields cannot have duplicate names.",
$"{prefix}.Nested"));
e("Fields cannot have duplicate names.", $"{fieldPrefix}.Nested");
}
}
}
if (command.Fields.Select(x => x.Name).Distinct().Count() != command.Fields.Count)
{
error(new ValidationError("Fields cannot have duplicate names.",
nameof(command.Fields)));
e("Fields cannot have duplicate names.", nameof(command.Fields));
}
}
});
@ -135,44 +97,27 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
if (command.ParentFieldId.HasValue)
{
var parentId = command.ParentFieldId.Value;
if (schema.FieldsById.TryGetValue(parentId, out var field) && field is IArrayField a)
{
arrayField = a;
}
else
{
throw new DomainObjectNotFoundException(parentId.ToString(), "Fields", typeof(Schema));
}
arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value);
}
Validate.It(() => "Cannot reorder schema fields.", error =>
{
if (command.FieldIds == null)
{
error(new ValidationError("Field ids is required.", nameof(command.FieldIds)));
error("Field ids is required.", nameof(command.FieldIds));
}
if (arrayField == null)
{
CheckFields(error, command, schema.FieldsById);
ValidateFieldIds(error, command, schema.FieldsById);
}
else
{
CheckFields(error, command, arrayField.FieldsById);
ValidateFieldIds(error, command, arrayField.FieldsById);
}
});
}
private static void CheckFields<T>(Action<ValidationError> error, ReorderFields c, IReadOnlyDictionary<long, T> fields)
{
if (c.FieldIds != null && (c.FieldIds.Count != fields.Count || c.FieldIds.Any(x => !fields.ContainsKey(x))))
{
error(new ValidationError("Field ids do not cover all fields.", nameof(c.FieldIds)));
}
}
public static void CanPublish(Schema schema, PublishSchema command)
{
Guard.NotNull(command, nameof(command));
@ -212,5 +157,32 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
}
private static void ValidateField(AddValidation e, string prefix, CreateSchemaFieldBase field)
{
if (!field.Name.IsPropertyName())
{
e("Field name must be a valid javascript property name.", $"{prefix}.{nameof(field.Name)}");
}
if (field.Properties == null)
{
e("Field properties is required.", $"{prefix}.{nameof(field.Properties)}");
}
else
{
var errors = FieldPropertiesValidator.Validate(field.Properties);
errors.Foreach(x => x.WithPrefix($"{prefix}.{nameof(field.Properties)}").AddTo(e));
}
}
private static void ValidateFieldIds<T>(AddValidation error, ReorderFields c, IReadOnlyDictionary<long, T> fields)
{
if (c.FieldIds != null && (c.FieldIds.Count != fields.Count || c.FieldIds.Any(x => !fields.ContainsKey(x))))
{
error("Field ids do not cover all fields.", nameof(c.FieldIds));
}
}
}
}

71
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs

@ -18,50 +18,43 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot add a new field.", error =>
Validate.It(() => "Cannot add a new field.", e =>
{
if (!command.Name.IsPropertyName())
{
error(new ValidationError("Name must be a valid javascript property name.", nameof(command.Name)));
e("Name must be a valid javascript property name.", nameof(command.Name));
}
if (command.Properties == null)
{
error(new ValidationError("Properties is required.", nameof(command.Properties)));
e("Properties is required.", nameof(command.Properties));
}
else
{
var errors = FieldPropertiesValidator.Validate(command.Properties);
foreach (var e in errors)
{
error(e.WithPrefix(nameof(command.Properties)));
}
errors.Foreach(x => x.WithPrefix(nameof(command.Properties)).AddTo(e));
}
if (command.ParentFieldId.HasValue)
{
var parentId = command.ParentFieldId.Value;
var arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value);
if (!schema.FieldsById.TryGetValue(parentId, out var rootField) || !(rootField is IArrayField arrayField))
{
throw new DomainObjectNotFoundException(parentId.ToString(), "Fields", typeof(Schema));
}
if (arrayField.FieldsByName.ContainsKey(command.Name))
{
error(new ValidationError($"A field with the same name already exists."));
e($"A field with the same name already exists.");
}
}
else
{
if (command.ParentFieldId == null && !command.Partitioning.IsValidPartitioning())
{
error(new ValidationError("Partitioning is not valid.", nameof(command.Partitioning)));
e("Partitioning is not valid.", nameof(command.Partitioning));
}
if (schema.FieldsByName.ContainsKey(command.Name))
{
error(new ValidationError($"A field with the same name already exists."));
e($"A field with the same name already exists.");
}
}
});
@ -71,27 +64,24 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsLocked)
{
throw new DomainException("Schema field is already locked.");
}
Validate.It(() => "Cannot update field.", error =>
Validate.It(() => "Cannot update field.", e =>
{
if (command.Properties == null)
{
error(new ValidationError("Properties is required.", nameof(command.Properties)));
e("Properties is required.", nameof(command.Properties));
}
else
{
var errors = FieldPropertiesValidator.Validate(command.Properties);
foreach (var e in errors)
{
error(e.WithPrefix(nameof(command.Properties)));
}
errors.Foreach(x => x.WithPrefix(nameof(command.Properties)).AddTo(e));
}
});
}
@ -100,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsLocked)
{
@ -117,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsDisabled)
{
@ -129,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsLocked)
{
@ -141,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (!field.IsHidden)
{
@ -153,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (!field.IsDisabled)
{
@ -165,37 +155,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsLocked)
{
throw new DomainException("Schema field is already locked.");
}
}
private static IField GetFieldOrThrow(Schema schema, long fieldId, long? parentId)
{
if (parentId.HasValue)
{
if (!schema.FieldsById.TryGetValue(parentId.Value, out var rootField) || !(rootField is IArrayField arrayField))
{
throw new DomainObjectNotFoundException(parentId.ToString(), "Fields", typeof(Schema));
}
if (!arrayField.FieldsById.TryGetValue(fieldId, out var nestedField))
{
throw new DomainObjectNotFoundException(fieldId.ToString(), $"Fields[{parentId}].Fields", typeof(Schema));
}
return nestedField;
}
if (!schema.FieldsById.TryGetValue(fieldId, out var field))
{
throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Schema));
}
return field;
}
}
}

17
src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs

@ -5,25 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using FieldNested = System.Collections.Generic.List<Squidex.Domain.Apps.Events.Schemas.SchemaCreatedNestedField>;
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Events.Schemas
{
public sealed class SchemaCreatedField
public sealed class SchemaCreatedField : SchemaCreatedFieldBase
{
public string Partitioning { get; set; }
public string Name { get; set; }
public bool IsHidden { get; set; }
public bool IsLocked { get; set; }
public bool IsDisabled { get; set; }
public FieldNested Nested { get; set; }
public FieldProperties Properties { get; set; }
public List<SchemaCreatedNestedField> Nested { get; set; }
}
}

24
src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Events.Schemas
{
public abstract class SchemaCreatedFieldBase
{
public string Name { get; set; }
public bool IsHidden { get; set; }
public bool IsLocked { get; set; }
public bool IsDisabled { get; set; }
public FieldProperties Properties { get; set; }
}
}

11
src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs

@ -9,16 +9,7 @@ using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Events.Schemas
{
public sealed class SchemaCreatedNestedField
public sealed class SchemaCreatedNestedField : SchemaCreatedFieldBase
{
public string Name { get; set; }
public bool IsHidden { get; set; }
public bool IsLocked { get; set; }
public bool IsDisabled { get; set; }
public FieldProperties Properties { get; set; }
}
}

38
src/Squidex.Infrastructure/Validate.cs

@ -12,27 +12,49 @@ using System.Threading.Tasks;
namespace Squidex.Infrastructure
{
public delegate void AddValidation(string message, params string[] propertyNames);
public static class Validate
{
public static void It(Func<string> message, Action<Action<ValidationError>> action)
public static void It(Func<string> message, Action<AddValidation> action)
{
var errors = new List<ValidationError>();
List<ValidationError> errors = null;
var addValidation = new AddValidation((m, p) =>
{
if (errors == null)
{
errors = new List<ValidationError>();
}
errors.Add(new ValidationError(m, p));
});
action(errors.Add);
action(addValidation);
if (errors.Any())
if (errors != null)
{
throw new ValidationException(message(), errors);
}
}
public static async Task It(Func<string> message, Func<Action<ValidationError>, Task> action)
public static async Task It(Func<string> message, Func<AddValidation, Task> action)
{
var errors = new List<ValidationError>();
List<ValidationError> errors = null;
var addValidation = new AddValidation((m, p) =>
{
if (errors == null)
{
errors = new List<ValidationError>();
}
errors.Add(new ValidationError(m, p));
});
await action(errors.Add);
await action(addValidation);
if (errors.Any())
if (errors != null)
{
throw new ValidationException(message(), errors);
}

5
src/Squidex.Infrastructure/ValidationError.cs

@ -46,5 +46,10 @@ namespace Squidex.Infrastructure
return new ValidationError(Message, prefix);
}
}
public void AddTo(AddValidation e)
{
e(Message, propertyNames);
}
}
}

4
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Apps;
@ -91,8 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new AssignContributor { ContributorId = "3", Permission = AppContributorPermission.Editor, Actor = new RefToken("user", "3") };
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan),
new ValidationError("You cannot change your own permission."));
return Assert.ThrowsAsync<SecurityException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan));
}
[Fact]

4
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs

@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var command = new AddPattern { PatternId = patternId, Name = "any", Pattern = ".*" };
ValidationAssert.Throws(() => GuardAppPattern.CanAdd(patterns_1, command),
new ValidationError("An pattern with the same name already exists."));
new ValidationError("A pattern with the same name already exists."));
}
[Fact]
@ -141,7 +141,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var command = new UpdatePattern { PatternId = id2, Name = "Pattern1", Pattern = "[0-4]" };
ValidationAssert.Throws(() => GuardAppPattern.CanUpdate(patterns_2, command),
new ValidationError("An pattern with the same name already exists."));
new ValidationError("A pattern with the same name already exists."));
}
[Fact]

56
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs

@ -207,7 +207,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
};
return ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider),
new ValidationError("Nested field name must be a valid javascript property name.",
new ValidationError("Field name must be a valid javascript property name.",
"Fields[1].Nested[1].Name"));
}
@ -238,12 +238,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
};
return ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider),
new ValidationError("Nested field properties is required.",
new ValidationError("Field properties is required.",
"Fields[1].Nested[1].Properties"));
}
[Fact]
public Task CanCreate_should_throw_exception_if_nested_field_properties_not_valid()
public Task CanCreate_should_throw_exception_if_nested_field_is_array()
{
var command = new CreateSchema
{
@ -260,7 +260,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
new CreateSchemaNestedField
{
Name = "nested1",
Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }
Properties = new ArrayFieldProperties()
}
}
}
@ -269,13 +269,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
};
return ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider),
new ValidationError("Max length must be greater than min length.",
"Fields[1].Nested[1].Properties.MinLength",
"Fields[1].Nested[1].Properties.MaxLength"));
new ValidationError("Nested field cannot be array fields.",
"Fields[1].Nested[1].Properties"));
}
[Fact]
public Task CanCreate_should_throw_exception_if_nested_field_have_duplicate_names()
public Task CanCreate_should_throw_exception_if_nested_field_properties_not_valid()
{
var command = new CreateSchema
{
@ -292,12 +291,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
new CreateSchemaNestedField
{
Name = "nested1",
Properties = new StringFieldProperties()
},
new CreateSchemaNestedField
{
Name = "nested1",
Properties = new StringFieldProperties()
Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }
}
}
}
@ -306,14 +300,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
};
return ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider),
new ValidationError("Fields cannot have duplicate names.",
"Fields[1].Nested"));
new ValidationError("Max length must be greater than min length.",
"Fields[1].Nested[1].Properties.MinLength",
"Fields[1].Nested[1].Properties.MaxLength"));
}
/*
[Fact]
public Task CanCreate_should_throw_exception_if_fields_contain_duplicate_names()
public Task CanCreate_should_throw_exception_if_nested_field_have_duplicate_names()
{
var command = new CreateSchema
{
@ -322,32 +315,20 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
new CreateSchemaField
{
Name = "field1",
Properties = ValidProperties(),
Partitioning = "invariant"
},
new CreateSchemaField
{
Name = "field1",
Properties = ValidProperties(),
Partitioning = "invariant"
},
new CreateSchemaField
{
Name = "field1",
Name = "array",
Properties = new ArrayFieldProperties(),
Partitioning = "invariant",
Partitioning = Partitioning.Invariant.Key,
Nested = new List<CreateSchemaNestedField>
{
new CreateSchemaNestedField
{
Name = "nested1",
Properties = ValidProperties()
Properties = new StringFieldProperties()
},
new CreateSchemaNestedField
{
Name = "nested1",
Properties = ValidProperties()
Properties = new StringFieldProperties()
}
}
}
@ -355,9 +336,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
Name = "new-schema"
};
return ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider));
return ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider),
new ValidationError("Fields cannot have duplicate names.",
"Fields[1].Nested"));
}
*/
[Fact]
public Task CanCreate_should_not_throw_exception_if_command_is_valid()

Loading…
Cancel
Save