Browse Source

Uniqueness validator.

pull/337/head
Sebastian Stehle 7 years ago
parent
commit
f15fed3f56
  1. 1
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs
  2. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
  3. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
  4. 46
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs
  5. 15
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs
  6. 51
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs
  7. 10
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs
  8. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  9. 10
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs
  10. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  11. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  12. 50
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs
  13. 60
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
  14. 13
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  15. 13
      src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  16. 2
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  17. 7
      src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs
  18. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs
  19. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs
  20. 11
      src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.html
  21. 16
      src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts
  22. 11
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html
  23. 13
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts
  24. 2
      src/Squidex/app/shared/services/schemas.types.ts
  25. 10
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs
  26. 12
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs
  27. 4
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs
  28. 10
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs
  29. 8
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs
  30. 2
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs
  31. 22
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs
  32. 26
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs
  33. 24
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs
  34. 12
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs
  35. 14
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs
  36. 93
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs

1
src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs

@ -7,7 +7,6 @@
using System; using System;
using Newtonsoft.Json; using Newtonsoft.Json;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.Schemas.Json namespace Squidex.Domain.Apps.Core.Schemas.Json

2
src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs

@ -21,6 +21,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public double? DefaultValue { get; set; } public double? DefaultValue { get; set; }
public bool IsUnique { get; set; }
public bool InlineEditable { get; set; } public bool InlineEditable { get; set; }
public NumberFieldEditor Editor { get; set; } public NumberFieldEditor Editor { get; set; }

2
src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs

@ -19,6 +19,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public int? MaxLength { get; set; } public int? MaxLength { get; set; }
public bool IsUnique { get; set; }
public bool InlineEditable { get; set; } public bool InlineEditable { get; set; }
public string DefaultValue { get; set; } public string DefaultValue { get; set; }

46
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs

@ -10,13 +10,20 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.ValidateContent namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
public delegate Task<IReadOnlyList<Guid>> CheckContents(Guid schemaId, FilterNode filter);
public delegate Task<IReadOnlyList<IAssetInfo>> CheckAssets(IEnumerable<Guid> ids);
public sealed class ValidationContext public sealed class ValidationContext
{ {
private readonly Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent; private readonly Guid contentId;
private readonly Func<IEnumerable<Guid>, Task<IReadOnlyList<IAssetInfo>>> checkAsset; private readonly Guid schemaId;
private readonly CheckContents checkContent;
private readonly CheckAssets checkAsset;
private readonly ImmutableQueue<string> propertyPath; private readonly ImmutableQueue<string> propertyPath;
public ImmutableQueue<string> Path public ImmutableQueue<string> Path
@ -24,18 +31,32 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
get { return propertyPath; } get { return propertyPath; }
} }
public Guid ContentId
{
get { return contentId; }
}
public Guid SchemaId
{
get { return schemaId; }
}
public bool IsOptional { get; } public bool IsOptional { get; }
public ValidationContext( public ValidationContext(
Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent, Guid contentId,
Func<IEnumerable<Guid>, Task<IReadOnlyList<IAssetInfo>>> checkAsset) Guid schemaId,
: this(checkContent, checkAsset, ImmutableQueue<string>.Empty, false) CheckContents checkContent,
CheckAssets checkAsset)
: this(contentId, schemaId, checkContent, checkAsset, ImmutableQueue<string>.Empty, false)
{ {
} }
private ValidationContext( private ValidationContext(
Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent, Guid contentId,
Func<IEnumerable<Guid>, Task<IReadOnlyList<IAssetInfo>>> checkAsset, Guid schemaId,
CheckContents checkContent,
CheckAssets checkAsset,
ImmutableQueue<string> propertyPath, ImmutableQueue<string> propertyPath,
bool isOptional) bool isOptional)
{ {
@ -46,23 +67,26 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
this.checkContent = checkContent; this.checkContent = checkContent;
this.checkAsset = checkAsset; this.checkAsset = checkAsset;
this.contentId = contentId;
this.schemaId = schemaId;
IsOptional = isOptional; IsOptional = isOptional;
} }
public ValidationContext Optional(bool isOptional) public ValidationContext Optional(bool isOptional)
{ {
return isOptional == IsOptional ? this : new ValidationContext(checkContent, checkAsset, propertyPath, isOptional); return isOptional == IsOptional ? this : new ValidationContext(contentId, schemaId, checkContent, checkAsset, propertyPath, isOptional);
} }
public ValidationContext Nested(string property) public ValidationContext Nested(string property)
{ {
return new ValidationContext(checkContent, checkAsset, propertyPath.Enqueue(property), IsOptional); return new ValidationContext(contentId, schemaId, checkContent, checkAsset, propertyPath.Enqueue(property), IsOptional);
} }
public Task<IReadOnlyList<Guid>> GetInvalidContentIdsAsync(IEnumerable<Guid> contentIds, Guid schemaId) public Task<IReadOnlyList<Guid>> GetContentIdsAsync(Guid schemaId, FilterNode filter)
{ {
return checkContent(contentIds, schemaId); return checkContent(schemaId, filter);
} }
public Task<IReadOnlyList<IAssetInfo>> GetAssetInfosAsync(IEnumerable<Guid> assetId) public Task<IReadOnlyList<IAssetInfo>> GetAssetInfosAsync(IEnumerable<Guid> assetId)

15
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs

@ -7,12 +7,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public sealed class ReferencesValidator : IValidator public sealed class ReferencesValidator : IValidator
{ {
private static readonly IReadOnlyList<string> Path = new List<string> { "Id" };
private readonly Guid schemaId; private readonly Guid schemaId;
public ReferencesValidator(Guid schemaId) public ReferencesValidator(Guid schemaId)
@ -24,11 +28,16 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
if (value is ICollection<Guid> contentIds) if (value is ICollection<Guid> contentIds)
{ {
var invalidIds = await context.GetInvalidContentIdsAsync(contentIds, schemaId); var filter = new FilterComparison(Path, FilterOperator.In, new FilterValue(contentIds.ToList()));
var foundIds = await context.GetContentIdsAsync(schemaId, filter);
foreach (var invalidId in invalidIds) foreach (var id in contentIds)
{ {
addError(context.Path, $"Contains invalid reference '{invalidId}'."); if (!foundIds.Contains(id))
{
addError(context.Path, $"Contains invalid reference '{id}'.");
}
} }
} }
} }

51
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs

@ -0,0 +1,51 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class UniqueValidator : IValidator
{
public async Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
var count = context.Path.Count();
if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Instance.Master.Key)))
{
FilterNode filter = null;
if (value is string s)
{
filter = new FilterComparison(Path(context), FilterOperator.Equals, new FilterValue(s));
}
else if (value is double d)
{
filter = new FilterComparison(Path(context), FilterOperator.Equals, new FilterValue(d));
}
if (filter != null)
{
var found = await context.GetContentIdsAsync(context.SchemaId, filter);
if (found.Any(x => x != context.ContentId))
{
addError(context.Path, "Another content with the same value exists.");
}
}
}
}
private static List<string> Path(ValidationContext context)
{
return Enumerable.Repeat("Data", 1).Union(context.Path).ToList();
}
}
}

10
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs

@ -111,6 +111,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
yield return new AllowedValuesValidator<double>(field.Properties.AllowedValues); yield return new AllowedValuesValidator<double>(field.Properties.AllowedValues);
} }
if (field.Properties.IsUnique)
{
yield return new UniqueValidator();
}
} }
public IEnumerable<IValidator> Visit(IField<ReferencesFieldProperties> field) public IEnumerable<IValidator> Visit(IField<ReferencesFieldProperties> field)
@ -147,6 +152,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
yield return new AllowedValuesValidator<string>(field.Properties.AllowedValues); yield return new AllowedValuesValidator<string>(field.Properties.AllowedValues);
} }
if (field.Properties.IsUnique)
{
yield return new UniqueValidator();
}
} }
public IEnumerable<IValidator> Visit(IField<TagsFieldProperties> field) public IEnumerable<IValidator> Visit(IField<TagsFieldProperties> field)

2
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
query = query.AdjustToModel(schema.SchemaDef, useDraft); query = query.AdjustToModel(schema.SchemaDef, useDraft);
var filter = FindExtensions.BuildQuery(query, schema.Id, status); var filter = query.ToFilter(schema.Id, status);
var contentCount = Collection.Find(filter).CountDocumentsAsync(); var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems = var contentItems =

10
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs

@ -16,9 +16,11 @@ using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
@ -52,13 +54,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await base.SetupCollectionAsync(collection, ct); await base.SetupCollectionAsync(collection, ct);
} }
public async Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> ids) public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, ISchemaEntity schema, FilterNode filterNode)
{ {
var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id);
var contentEntities = var contentEntities =
await Collection.Find(x => x.IndexedSchemaId == schemaId && ids.Contains(x.Id) && x.IsDeleted != true).Only(x => x.Id) await Collection.Find(filter).Only(x => x.Id)
.ToListAsync(); .ToListAsync();
return ids.Except(contentEntities.Select(x => Guid.Parse(x["_id"].AsString))).ToList(); return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
} }
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId) public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId)

2
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
private NamedContentData dataDraft; private NamedContentData dataDraft;
[BsonId] [BsonId]
[BsonElement] [BsonElement("_id")]
[BsonRepresentation(BsonType.String)] [BsonRepresentation(BsonType.String)]
public Guid Id { get; set; } public Guid Id { get; set; }

4
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -91,11 +91,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
} }
public async Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> ids) public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode)
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
return await contentsDraft.QueryNotFoundAsync(appId, schemaId, ids); return await contentsDraft.QueryIdsAsync(appId, await appProvider.GetSchemaAsync(appId, schemaId), filterNode);
} }
} }

50
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs

@ -0,0 +1,50 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using NodaTime;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{
internal sealed class AdaptionVisitor : TransformVisitor
{
private readonly Func<IReadOnlyList<string>, IReadOnlyList<string>> pathConverter;
public AdaptionVisitor(Func<IReadOnlyList<string>, IReadOnlyList<string>> pathConverter)
{
this.pathConverter = pathConverter;
}
public override FilterNode Visit(FilterComparison nodeIn)
{
FilterComparison result;
var value = nodeIn.Rhs.Value;
if (value is Instant &&
!string.Equals(nodeIn.Lhs[0], "mt", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(nodeIn.Lhs[0], "ct", StringComparison.OrdinalIgnoreCase))
{
result = new FilterComparison(pathConverter(nodeIn.Lhs), nodeIn.Operator, new FilterValue(value.ToString()));
}
else
{
result = new FilterComparison(pathConverter(nodeIn.Lhs), nodeIn.Operator, nodeIn.Rhs);
}
if (result.Lhs.Count == 1 && result.Lhs[0] == "_id" && result.Rhs.Value is List<Guid> guidList)
{
result = new FilterComparison(nodeIn.Lhs, nodeIn.Operator, new FilterValue(guidList.Select(x => x.ToString()).ToList()));
}
return result;
}
}
}

60
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs

@ -11,7 +11,6 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver; using MongoDB.Driver;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
@ -28,33 +27,30 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
typeof(MongoContentEntity).GetProperties() typeof(MongoContentEntity).GetProperties()
.ToDictionary(x => x.Name, x => x.GetCustomAttribute<BsonElementAttribute>()?.ElementName ?? x.Name, StringComparer.OrdinalIgnoreCase); .ToDictionary(x => x.Name, x => x.GetCustomAttribute<BsonElementAttribute>()?.ElementName ?? x.Name, StringComparer.OrdinalIgnoreCase);
private sealed class AdaptionVisitor : TransformVisitor public static Query AdjustToModel(this Query query, Schema schema, bool useDraft)
{ {
private readonly Func<IReadOnlyList<string>, IReadOnlyList<string>> pathConverter; var pathConverter = PathConverter(schema, useDraft);
public AdaptionVisitor(Func<IReadOnlyList<string>, IReadOnlyList<string>> pathConverter) if (query.Filter != null)
{ {
this.pathConverter = pathConverter; query.Filter = query.Filter.Accept(new AdaptionVisitor(pathConverter));
} }
public override FilterNode Visit(FilterComparison nodeIn) query.Sort = query.Sort.Select(x => new SortNode(pathConverter(x.Path), x.SortOrder)).ToList();
{
var value = nodeIn.Rhs.Value;
if (value is Instant && return query;
!string.Equals(nodeIn.Lhs[0], "mt", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(nodeIn.Lhs[0], "ct", StringComparison.OrdinalIgnoreCase))
{
return new FilterComparison(pathConverter(nodeIn.Lhs), nodeIn.Operator, new FilterValue(value.ToString()));
} }
return new FilterComparison(pathConverter(nodeIn.Lhs), nodeIn.Operator, nodeIn.Rhs); public static FilterNode AdjustToModel(this FilterNode filterNode, Schema schema, bool useDraft)
} {
var pathConverter = PathConverter(schema, useDraft);
return filterNode.Accept(new AdaptionVisitor(pathConverter));
} }
public static Query AdjustToModel(this Query query, Schema schema, bool useDraft) private static Func<IReadOnlyList<string>, IReadOnlyList<string>> PathConverter(Schema schema, bool useDraft)
{ {
var pathConverter = new Func<IReadOnlyList<string>, IReadOnlyList<string>>(propertyNames => return new Func<IReadOnlyList<string>, IReadOnlyList<string>>(propertyNames =>
{ {
var result = new List<string>(propertyNames); var result = new List<string>(propertyNames);
@ -96,15 +92,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
return result; return result;
}); });
if (query.Filter != null)
{
query.Filter = query.Filter.Accept(new AdaptionVisitor(pathConverter));
}
query.Sort = query.Sort.Select(x => new SortNode(pathConverter(x.Path), x.SortOrder)).ToList();
return query;
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, Query query) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, Query query)
@ -122,16 +109,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
return cursor.Skip(query); return cursor.Skip(query);
} }
public static FilterDefinition<MongoContentEntity> BuildQuery(Query query, Guid schemaId, Status[] status) public static FilterDefinition<MongoContentEntity> ToFilter(this Query query, Guid schemaId, Status[] status)
{ {
var filters = new List<FilterDefinition<MongoContentEntity>> var filters = new List<FilterDefinition<MongoContentEntity>>
{ {
Filter.Eq(x => x.IndexedSchemaId, schemaId) Filter.Eq(x => x.IndexedSchemaId, schemaId),
Filter.Ne(x => x.IsDeleted, true)
}; };
if (status != null) if (status != null)
{ {
filters.Add(Filter.Ne(x => x.IsDeleted, true));
filters.Add(Filter.In(x => x.Status, status)); filters.Add(Filter.In(x => x.Status, status));
} }
@ -149,14 +136,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
} }
} }
if (filters.Count == 1) return Filter.And(filters);
{
return filters[0];
} }
else
public static FilterDefinition<MongoContentEntity> ToFilter(this FilterNode filterNode, Guid schemaId)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{ {
Filter.Eq(x => x.IndexedSchemaId, schemaId),
Filter.Ne(x => x.IsDeleted, true),
filterNode.BuildFilter<MongoContentEntity>()
};
return Filter.And(filters); return Filter.And(filters);
} }
} }
}
} }

13
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
case CreateContent createContent: case CreateContent createContent:
return CreateReturnAsync(createContent, async c => return CreateReturnAsync(createContent, async c =>
{ {
var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, () => "Failed to create content."); var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, Guid.Empty, () => "Failed to create content.");
GuardContent.CanCreate(ctx.Schema, c); GuardContent.CanCreate(ctx.Schema, c);
@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
try try
{ {
var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, () => "Failed to change content."); var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, Snapshot.Id, () => "Failed to change content.");
GuardContent.CanChangeContentStatus(ctx.Schema, Snapshot.IsPending, Snapshot.Status, c); GuardContent.CanChangeContentStatus(ctx.Schema, Snapshot.IsPending, Snapshot.Status, c);
@ -162,7 +162,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
case DeleteContent deleteContent: case DeleteContent deleteContent:
return UpdateAsync(deleteContent, async c => return UpdateAsync(deleteContent, async c =>
{ {
var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, () => "Failed to delete content."); var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, Snapshot.Id, () => "Failed to delete content.");
GuardContent.CanDelete(ctx.Schema, c); GuardContent.CanDelete(ctx.Schema, c);
@ -197,7 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!currentData.Equals(newData)) if (!currentData.Equals(newData))
{ {
var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, () => "Failed to update content."); var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, Snapshot.Id, () => "Failed to update content.");
if (partial) if (partial)
{ {
@ -301,11 +301,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
return Snapshot.Apply(@event); return Snapshot.Apply(@event);
} }
private async Task<ContentOperationContext> CreateContext(Guid appId, Guid schemaId, Func<string> message) private async Task<ContentOperationContext> CreateContext(Guid appId, Guid schemaId, Guid contentId, Func<string> message)
{ {
var operationContext = var operationContext =
await ContentOperationContext.CreateAsync( await ContentOperationContext.CreateAsync(appId, schemaId, contentId,
appId, schemaId,
appProvider, assetRepository, contentRepository, scriptEngine, message); appProvider, assetRepository, contentRepository, scriptEngine, message);
return operationContext; return operationContext;

13
src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -7,7 +7,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.EnrichContent; using Squidex.Domain.Apps.Core.EnrichContent;
@ -18,6 +17,7 @@ using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
@ -29,6 +29,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
private IScriptEngine scriptEngine; private IScriptEngine scriptEngine;
private ISchemaEntity schemaEntity; private ISchemaEntity schemaEntity;
private IAppEntity appEntity; private IAppEntity appEntity;
private Guid contentId;
private Guid schemaId;
private Func<string> message; private Func<string> message;
public ISchemaEntity Schema public ISchemaEntity Schema
@ -39,6 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public static async Task<ContentOperationContext> CreateAsync( public static async Task<ContentOperationContext> CreateAsync(
Guid appId, Guid appId,
Guid schemaId, Guid schemaId,
Guid contentId,
IAppProvider appProvider, IAppProvider appProvider,
IAssetRepository assetRepository, IAssetRepository assetRepository,
IContentRepository contentRepository, IContentRepository contentRepository,
@ -51,8 +54,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
appEntity = appEntity, appEntity = appEntity,
assetRepository = assetRepository, assetRepository = assetRepository,
contentId = contentId,
contentRepository = contentRepository, contentRepository = contentRepository,
message = message, message = message,
schemaId = schemaId,
schemaEntity = schemaEntity, schemaEntity = schemaEntity,
scriptEngine = scriptEngine scriptEngine = scriptEngine
}; };
@ -106,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private ValidationContext CreateValidationContext() private ValidationContext CreateValidationContext()
{ {
return new ValidationContext((contentIds, schemaId) => QueryContentsAsync(schemaId, contentIds), QueryAssetsAsync); return new ValidationContext(contentId, schemaId, (sid, filterNode) => QueryContentsAsync(sid, filterNode), QueryAssetsAsync);
} }
private async Task<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(IEnumerable<Guid> assetIds) private async Task<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(IEnumerable<Guid> assetIds)
@ -114,9 +119,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
return await assetRepository.QueryAsync(appEntity.Id, new HashSet<Guid>(assetIds)); return await assetRepository.QueryAsync(appEntity.Id, new HashSet<Guid>(assetIds));
} }
private async Task<IReadOnlyList<Guid>> QueryContentsAsync(Guid schemaId, IEnumerable<Guid> contentIds) private async Task<IReadOnlyList<Guid>> QueryContentsAsync(Guid filterSchemaId, FilterNode filterNode)
{ {
return await contentRepository.QueryNotFoundAsync(appEntity.Id, schemaId, contentIds.ToList()); return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode);
} }
} }
} }

2
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query);
Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> ids); Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id); Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id);

7
src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs

@ -26,10 +26,15 @@ namespace Squidex.Infrastructure.MongoDb.Queries
if (query.Filter != null) if (query.Filter != null)
{ {
return (FilterVisitor<T>.Visit(query.Filter), true); return (query.Filter.BuildFilter<T>(), true);
} }
return (null, false); return (null, false);
} }
public static FilterDefinition<T> BuildFilter<T>(this FilterNode filterNode)
{
return FilterVisitor<T>.Visit(filterNode);
}
} }
} }

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs

@ -37,6 +37,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary> /// </summary>
public double[] AllowedValues { get; set; } public double[] AllowedValues { get; set; }
/// <summary>
/// Indicates if the field value must be unique. Ignored for nested fields and localized fields.
/// </summary>
public bool IsUnique { get; set; }
/// <summary> /// <summary>
/// Indicates that the inline editor is enabled for this field. /// Indicates that the inline editor is enabled for this field.
/// </summary> /// </summary>

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs

@ -47,6 +47,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary> /// </summary>
public string[] AllowedValues { get; set; } public string[] AllowedValues { get; set; }
/// <summary>
/// Indicates if the field value must be unique. Ignored for nested fields and localized fields.
/// </summary>
public bool IsUnique { get; set; }
/// <summary> /// <summary>
/// Indicates that the inline editor is enabled for this field. /// Indicates that the inline editor is enabled for this field.
/// </summary> /// </summary>

11
src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.html

@ -1,4 +1,15 @@
<div [formGroup]="editForm"> <div [formGroup]="editForm">
<div class="form-group row" *ngIf="showUnique">
<div class="col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldUnique" formControlName="isUnique" />
<label class="form-check-label" for="{{field.fieldId}}_fieldUnique">
Unique
</label>
</div>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<div class="col-9 offset-3"> <div class="col-9 offset-3">
<div class="form-check"> <div class="form-check">

16
src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts

@ -10,7 +10,12 @@ import { FormControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators'; import { map, startWith } from 'rxjs/operators';
import { FieldDto, NumberFieldPropertiesDto } from '@app/shared'; import {
FieldDto,
NumberFieldPropertiesDto,
RootFieldDto,
Types
} from '@app/shared';
@Component({ @Component({
selector: 'sqx-number-validation', selector: 'sqx-number-validation',
@ -27,9 +32,18 @@ export class NumberValidationComponent implements OnInit {
@Input() @Input()
public properties: NumberFieldPropertiesDto; public properties: NumberFieldPropertiesDto;
public showUnique: boolean;
public showDefaultValue: Observable<boolean>; public showDefaultValue: Observable<boolean>;
public ngOnInit() { public ngOnInit() {
this.showUnique = Types.is(this.field, RootFieldDto) && !this.field.isLocalizable;
if (this.showUnique) {
this.editForm.setControl('isUnique',
new FormControl(this.properties.isUnique));
}
this.editForm.setControl('maxValue', this.editForm.setControl('maxValue',
new FormControl(this.properties.maxValue)); new FormControl(this.properties.maxValue));

11
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html

@ -1,4 +1,15 @@
<div [formGroup]="editForm"> <div [formGroup]="editForm">
<div class="form-group row" *ngIf="showUnique">
<div class="col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldUnique" formControlName="isUnique" />
<label class="form-check-label" for="{{field.fieldId}}_fieldUnique">
Unique
</label>
</div>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<div class="col-9 offset-3"> <div class="col-9 offset-3">
<div class="form-check"> <div class="form-check">

13
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts

@ -15,7 +15,9 @@ import {
FieldDto, FieldDto,
ImmutableArray, ImmutableArray,
ModalModel, ModalModel,
StringFieldPropertiesDto RootFieldDto,
StringFieldPropertiesDto,
Types
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -45,11 +47,20 @@ export class StringValidationComponent implements OnDestroy, OnInit {
public patternName: string; public patternName: string;
public patternsModal = new ModalModel(); public patternsModal = new ModalModel();
public showUnique: boolean;
public ngOnDestroy() { public ngOnDestroy() {
this.patternSubscription.unsubscribe(); this.patternSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.showUnique = Types.is(this.field, RootFieldDto) && !this.field.isLocalizable;
if (this.showUnique) {
this.editForm.setControl('isUnique',
new FormControl(this.properties.isUnique));
}
this.editForm.setControl('maxLength', this.editForm.setControl('maxLength',
new FormControl(this.properties.maxLength)); new FormControl(this.properties.maxLength));

2
src/Squidex/app/shared/services/schemas.types.ts

@ -241,6 +241,7 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Number'; public readonly fieldType = 'Number';
public readonly inlineEditable: boolean = false; public readonly inlineEditable: boolean = false;
public readonly isUnique: boolean = false;
public readonly defaultValue?: number; public readonly defaultValue?: number;
public readonly maxValue?: number; public readonly maxValue?: number;
public readonly minValue?: number; public readonly minValue?: number;
@ -279,6 +280,7 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'String'; public readonly fieldType = 'String';
public readonly inlineEditable = false; public readonly inlineEditable = false;
public readonly isUnique: boolean = false;
public readonly defaultValue?: string; public readonly defaultValue?: string;
public readonly pattern?: string; public readonly pattern?: string;
public readonly patternMessage?: string; public readonly patternMessage?: string;

10
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs

@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_tags_are_required_and_null() public async Task Should_add_error_if_tags_are_required_and_null()
{ {
var sut = Field(new ArrayFieldProperties { IsRequired = true }); var sut = Field(new ArrayFieldProperties { IsRequired = true });
@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_tags_are_required_and_empty() public async Task Should_add_error_if_tags_are_required_and_empty()
{ {
var sut = Field(new ArrayFieldProperties { IsRequired = true }); var sut = Field(new ArrayFieldProperties { IsRequired = true });
@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_is_not_valid() public async Task Should_add_error_if_value_is_not_valid()
{ {
var sut = Field(new ArrayFieldProperties()); var sut = Field(new ArrayFieldProperties());
@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_has_not_enough_items() public async Task Should_add_error_if_value_has_not_enough_items()
{ {
var sut = Field(new ArrayFieldProperties { MinItems = 3 }); var sut = Field(new ArrayFieldProperties { MinItems = 3 });
@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_has_too_much_items() public async Task Should_add_error_if_value_has_too_much_items()
{ {
var sut = Field(new ArrayFieldProperties { MaxItems = 1 }); var sut = Field(new ArrayFieldProperties { MaxItems = 1 });

12
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs

@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_assets_are_required_and_null() public async Task Should_add_error_if_assets_are_required_and_null()
{ {
var sut = Field(new AssetsFieldProperties { IsRequired = true }); var sut = Field(new AssetsFieldProperties { IsRequired = true });
@ -104,7 +104,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_assets_are_required_and_empty() public async Task Should_add_error_if_assets_are_required_and_empty()
{ {
var sut = Field(new AssetsFieldProperties { IsRequired = true }); var sut = Field(new AssetsFieldProperties { IsRequired = true });
@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_is_not_valid() public async Task Should_add_error_if_value_is_not_valid()
{ {
var sut = Field(new AssetsFieldProperties()); var sut = Field(new AssetsFieldProperties());
@ -126,7 +126,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_has_not_enough_items() public async Task Should_add_error_if_value_has_not_enough_items()
{ {
var sut = Field(new AssetsFieldProperties { MinItems = 3 }); var sut = Field(new AssetsFieldProperties { MinItems = 3 });
@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_has_too_much_items() public async Task Should_add_error_if_value_has_too_much_items()
{ {
var sut = Field(new AssetsFieldProperties { MaxItems = 1 }); var sut = Field(new AssetsFieldProperties { MaxItems = 1 });
@ -148,7 +148,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_asset_are_not_valid() public async Task Should_add_error_if_asset_are_not_valid()
{ {
var assetId = Guid.NewGuid(); var assetId = Guid.NewGuid();

4
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs

@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_boolean_is_required() public async Task Should_add_error_if_boolean_is_required()
{ {
var sut = Field(new BooleanFieldProperties { IsRequired = true }); var sut = Field(new BooleanFieldProperties { IsRequired = true });
@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_is_not_valid() public async Task Should_add_error_if_value_is_not_valid()
{ {
var sut = Field(new BooleanFieldProperties()); var sut = Field(new BooleanFieldProperties());

10
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs

@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_datetime_is_required() public async Task Should_add_error_if_datetime_is_required()
{ {
var sut = Field(new DateTimeFieldProperties { IsRequired = true }); var sut = Field(new DateTimeFieldProperties { IsRequired = true });
@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_datetime_is_less_than_min() public async Task Should_add_error_if_datetime_is_less_than_min()
{ {
var sut = Field(new DateTimeFieldProperties { MinValue = FutureDays(10) }); var sut = Field(new DateTimeFieldProperties { MinValue = FutureDays(10) });
@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_datetime_is_greater_than_max() public async Task Should_add_error_if_datetime_is_greater_than_max()
{ {
var sut = Field(new DateTimeFieldProperties { MaxValue = FutureDays(10) }); var sut = Field(new DateTimeFieldProperties { MaxValue = FutureDays(10) });
@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_is_not_valid() public async Task Should_add_error_if_value_is_not_valid()
{ {
var sut = Field(new DateTimeFieldProperties()); var sut = Field(new DateTimeFieldProperties());
@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_is_another_type() public async Task Should_add_error_if_value_is_another_type()
{ {
var sut = Field(new DateTimeFieldProperties()); var sut = Field(new DateTimeFieldProperties());

8
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs

@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_geolocation_has_invalid_latitude() public async Task Should_add_error_if_geolocation_has_invalid_latitude()
{ {
var sut = Field(new GeolocationFieldProperties { IsRequired = true }); var sut = Field(new GeolocationFieldProperties { IsRequired = true });
@ -66,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_geolocation_has_invalid_longitude() public async Task Should_add_error_if_geolocation_has_invalid_longitude()
{ {
var sut = Field(new GeolocationFieldProperties { IsRequired = true }); var sut = Field(new GeolocationFieldProperties { IsRequired = true });
@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_geolocation_has_too_many_properties() public async Task Should_add_error_if_geolocation_has_too_many_properties()
{ {
var sut = Field(new GeolocationFieldProperties { IsRequired = true }); var sut = Field(new GeolocationFieldProperties { IsRequired = true });
@ -97,7 +97,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_geolocation_is_required() public async Task Should_add_error_if_geolocation_is_required()
{ {
var sut = Field(new GeolocationFieldProperties { IsRequired = true }); var sut = Field(new GeolocationFieldProperties { IsRequired = true });

2
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs

@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_json_is_required() public async Task Should_add_error_if_json_is_required()
{ {
var sut = Field(new JsonFieldProperties { IsRequired = true }); var sut = Field(new JsonFieldProperties { IsRequired = true });

22
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
@ -38,7 +39,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_number_is_required() public async Task Should_add_error_if_number_is_required()
{ {
var sut = Field(new NumberFieldProperties { IsRequired = true }); var sut = Field(new NumberFieldProperties { IsRequired = true });
@ -49,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_number_is_less_than_min() public async Task Should_add_error_if_number_is_less_than_min()
{ {
var sut = Field(new NumberFieldProperties { MinValue = 10 }); var sut = Field(new NumberFieldProperties { MinValue = 10 });
@ -60,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_number_is_greater_than_max() public async Task Should_add_error_if_number_is_greater_than_max()
{ {
var sut = Field(new NumberFieldProperties { MaxValue = 10 }); var sut = Field(new NumberFieldProperties { MaxValue = 10 });
@ -71,7 +72,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_number_is_not_allowed() public async Task Should_add_error_if_number_is_not_allowed()
{ {
var sut = Field(new NumberFieldProperties { AllowedValues = ReadOnlyCollection.Create(10d) }); var sut = Field(new NumberFieldProperties { AllowedValues = ReadOnlyCollection.Create(10d) });
@ -82,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_is_not_valid() public async Task Should_add_error_if_value_is_not_valid()
{ {
var sut = Field(new NumberFieldProperties()); var sut = Field(new NumberFieldProperties());
@ -92,6 +93,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new[] { "Not a valid value." }); new[] { "Not a valid value." });
} }
[Fact]
public async Task Should_add_error_if_unique_constraint_failed()
{
var sut = Field(new NumberFieldProperties { IsUnique = true });
await sut.ValidateAsync(CreateValue(12.5), errors, ValidationTestExtensions.References(Guid.NewGuid()));
errors.Should().BeEquivalentTo(
new[] { "Another content with the same value exists." });
}
private static JValue CreateValue(object v) private static JValue CreateValue(object v)
{ {
return new JValue(v); return new JValue(v);

26
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs

@ -20,6 +20,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
private readonly List<string> errors = new List<string>(); private readonly List<string> errors = new List<string>();
private readonly Guid schemaId = Guid.NewGuid(); private readonly Guid schemaId = Guid.NewGuid();
private readonly Guid ref1 = Guid.NewGuid();
private readonly Guid ref2 = Guid.NewGuid();
[Fact] [Fact]
public void Should_instantiate_field() public void Should_instantiate_field()
@ -34,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new ReferencesFieldProperties()); var sut = Field(new ReferencesFieldProperties());
await sut.ValidateAsync(CreateValue(Guid.NewGuid()), errors, ValidationTestExtensions.ValidContext); await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References(ref1));
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -50,7 +52,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_references_are_required_and_null() public async Task Should_add_error_if_references_are_required_and_null()
{ {
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true });
@ -61,7 +63,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_references_are_required_and_empty() public async Task Should_add_error_if_references_are_required_and_empty()
{ {
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true });
@ -72,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_is_not_valid() public async Task Should_add_error_if_value_is_not_valid()
{ {
var sut = Field(new ReferencesFieldProperties()); var sut = Field(new ReferencesFieldProperties());
@ -83,38 +85,36 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_has_not_enough_items() public async Task Should_add_error_if_value_has_not_enough_items()
{ {
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 }); var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 });
await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); await sut.ValidateAsync(CreateValue(ref1, ref2), errors, ValidationTestExtensions.References(ref1, ref2));
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Must have at least 3 item(s)." }); new[] { "Must have at least 3 item(s)." });
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_has_too_much_items() public async Task Should_add_error_if_value_has_too_much_items()
{ {
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 }); var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 });
await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); await sut.ValidateAsync(CreateValue(ref1, ref2), errors, ValidationTestExtensions.References(ref1, ref2));
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Must have not more than 1 item(s)." }); new[] { "Must have not more than 1 item(s)." });
} }
[Fact] [Fact]
public async Task Should_add_errors_if_reference_are_not_valid() public async Task Should_add_error_if_reference_are_not_valid()
{ {
var referenceId = Guid.NewGuid();
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId });
await sut.ValidateAsync(CreateValue(referenceId), errors, ValidationTestExtensions.InvalidReferences(referenceId)); await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References());
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { $"Contains invalid reference '{referenceId}'." }); new[] { $"Contains invalid reference '{ref1}'." });
} }
private static JToken CreateValue(params Guid[] ids) private static JToken CreateValue(params Guid[] ids)

24
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
@ -38,7 +39,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_string_is_required() public async Task Should_add_error_if_string_is_required()
{ {
var sut = Field(new StringFieldProperties { IsRequired = true }); var sut = Field(new StringFieldProperties { IsRequired = true });
@ -49,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_string_is_shorter_than_min_length() public async Task Should_add_error_if_string_is_shorter_than_min_length()
{ {
var sut = Field(new StringFieldProperties { MinLength = 10 }); var sut = Field(new StringFieldProperties { MinLength = 10 });
@ -60,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_string_is_longer_than_max_length() public async Task Should_add_error_if_string_is_longer_than_max_length()
{ {
var sut = Field(new StringFieldProperties { MaxLength = 5 }); var sut = Field(new StringFieldProperties { MaxLength = 5 });
@ -71,7 +72,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_string_not_allowed() public async Task Should_add_error_if_string_not_allowed()
{ {
var sut = Field(new StringFieldProperties { AllowedValues = ReadOnlyCollection.Create("Foo") }); var sut = Field(new StringFieldProperties { AllowedValues = ReadOnlyCollection.Create("Foo") });
@ -82,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_number_is_not_valid_pattern() public async Task Should_add_error_if_number_is_not_valid_pattern()
{ {
var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}" }); var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}" });
@ -93,7 +94,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_number_is_not_valid_pattern_with_message() public async Task Should_add_error_if_number_is_not_valid_pattern_with_message()
{ {
var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message." }); var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message." });
@ -103,6 +104,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new[] { "Custom Error Message." }); new[] { "Custom Error Message." });
} }
[Fact]
public async Task Should_add_error_if_unique_constraint_failed()
{
var sut = Field(new StringFieldProperties { IsUnique = true });
await sut.ValidateAsync(CreateValue("abc"), errors, ValidationTestExtensions.References(Guid.NewGuid()));
errors.Should().BeEquivalentTo(
new[] { "Another content with the same value exists." });
}
private static JValue CreateValue(object v) private static JValue CreateValue(object v)
{ {
return new JValue(v); return new JValue(v);

12
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs

@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_tags_are_required_and_null() public async Task Should_add_error_if_tags_are_required_and_null()
{ {
var sut = Field(new TagsFieldProperties { IsRequired = true }); var sut = Field(new TagsFieldProperties { IsRequired = true });
@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_tags_are_required_and_empty() public async Task Should_add_error_if_tags_are_required_and_empty()
{ {
var sut = Field(new TagsFieldProperties { IsRequired = true }); var sut = Field(new TagsFieldProperties { IsRequired = true });
@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_is_not_valid() public async Task Should_add_error_if_value_is_not_valid()
{ {
var sut = Field(new TagsFieldProperties()); var sut = Field(new TagsFieldProperties());
@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_has_not_enough_items() public async Task Should_add_error_if_value_has_not_enough_items()
{ {
var sut = Field(new TagsFieldProperties { MinItems = 3 }); var sut = Field(new TagsFieldProperties { MinItems = 3 });
@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_has_too_much_items() public async Task Should_add_error_if_value_has_too_much_items()
{ {
var sut = Field(new TagsFieldProperties { MaxItems = 1 }); var sut = Field(new TagsFieldProperties { MaxItems = 1 });
@ -104,7 +104,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
} }
[Fact] [Fact]
public async Task Should_add_errors_if_value_contains_an_not_allowed_values() public async Task Should_add_error_if_value_contains_an_not_allowed_values()
{ {
var sut = Field(new TagsFieldProperties { AllowedValues = ReadOnlyCollection.Create("tag-2", "tag-3") }); var sut = Field(new TagsFieldProperties { AllowedValues = ReadOnlyCollection.Create("tag-2", "tag-3") });

14
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs

@ -18,10 +18,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
public static class ValidationTestExtensions public static class ValidationTestExtensions
{ {
private static readonly Task<IReadOnlyList<Guid>> ValidReferences = Task.FromResult<IReadOnlyList<Guid>>(new List<Guid>()); private static readonly Task<IReadOnlyList<Guid>> EmptyReferences = Task.FromResult<IReadOnlyList<Guid>>(new List<Guid>());
private static readonly Task<IReadOnlyList<IAssetInfo>> ValidAssets = Task.FromResult<IReadOnlyList<IAssetInfo>>(new List<IAssetInfo>()); private static readonly Task<IReadOnlyList<IAssetInfo>> EmptyAssets = Task.FromResult<IReadOnlyList<IAssetInfo>>(new List<IAssetInfo>());
public static readonly ValidationContext ValidContext = new ValidationContext((x, y) => ValidReferences, x => ValidAssets); public static readonly ValidationContext ValidContext = new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => EmptyAssets);
public static Task ValidateAsync(this IValidator validator, object value, IList<string> errors, ValidationContext context = null) public static Task ValidateAsync(this IValidator validator, object value, IList<string> errors, ValidationContext context = null)
{ {
@ -75,14 +75,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var actual = Task.FromResult<IReadOnlyList<IAssetInfo>>(assets.ToList()); var actual = Task.FromResult<IReadOnlyList<IAssetInfo>>(assets.ToList());
return new ValidationContext((x, y) => ValidReferences, x => actual); return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => actual);
} }
public static ValidationContext InvalidReferences(Guid referencesIds) public static ValidationContext References(params Guid[] referencesIds)
{ {
var actual = Task.FromResult<IReadOnlyList<Guid>>(new List<Guid> { referencesIds }); var actual = Task.FromResult<IReadOnlyList<Guid>>(referencesIds.ToList());
return new ValidationContext((x, y) => actual, x => ValidAssets); return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => actual, x => EmptyAssets);
} }
} }
} }

93
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs

@ -0,0 +1,93 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{
public class UniqueValidatorTests
{
private readonly List<string> errors = new List<string>();
private readonly Guid contentId = Guid.NewGuid();
private readonly Guid schemaId = Guid.NewGuid();
[Fact]
public async Task Should_add_error_if_string_value_not_found()
{
var sut = new UniqueValidator();
var filter = string.Empty;
await sut.ValidateAsync("hi", errors, Context(Guid.NewGuid(), f => filter = f));
errors.Should().BeEquivalentTo(
new[] { "property: Another content with the same value exists." });
Assert.Equal("Data.property.iv == 'hi'", filter);
}
[Fact]
public async Task Should_add_error_if_double_value_not_found()
{
var sut = new UniqueValidator();
var filter = string.Empty;
await sut.ValidateAsync(12.5, errors, Context(Guid.NewGuid(), f => filter = f));
errors.Should().BeEquivalentTo(
new[] { "property: Another content with the same value exists." });
Assert.Equal("Data.property.iv == 12.5", filter);
}
[Fact]
public async Task Should_not_add_error_if_string_value_found()
{
var sut = new UniqueValidator();
var filter = string.Empty;
await sut.ValidateAsync("hi", errors, Context(contentId, f => filter = f));
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_double_value_found()
{
var sut = new UniqueValidator();
var filter = string.Empty;
await sut.ValidateAsync(12.5, errors, Context(contentId, f => filter = f));
Assert.Empty(errors);
}
private ValidationContext Context(Guid id, Action<string> filter)
{
return new ValidationContext(contentId, schemaId,
(schema, filterNode) =>
{
filter(filterNode.ToString());
return Task.FromResult<IReadOnlyList<Guid>>(new List<Guid> { id });
},
ids =>
{
return Task.FromResult<IReadOnlyList<IAssetInfo>>(new List<IAssetInfo>());
}).Nested("property").Nested("iv");
}
}
}
Loading…
Cancel
Save