mirror of https://github.com/Squidex/squidex.git
52 changed files with 791 additions and 181 deletions
@ -0,0 +1,92 @@ |
|||
// ==========================================================================
|
|||
// ReferencesField.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Microsoft.OData.Edm; |
|||
using Newtonsoft.Json.Linq; |
|||
using NJsonSchema; |
|||
using Squidex.Core.Schemas.Validators; |
|||
|
|||
namespace Squidex.Core.Schemas |
|||
{ |
|||
public sealed class ReferencesField : Field<ReferencesFieldProperties>, IReferenceField |
|||
{ |
|||
private static readonly Guid[] EmptyIds = new Guid[0]; |
|||
|
|||
public ReferencesField(long id, string name, Partitioning partitioning) |
|||
: this(id, name, partitioning, new ReferencesFieldProperties()) |
|||
{ |
|||
} |
|||
|
|||
public ReferencesField(long id, string name, Partitioning partitioning, ReferencesFieldProperties properties) |
|||
: base(id, name, partitioning, properties) |
|||
{ |
|||
} |
|||
|
|||
protected override IEnumerable<IValidator> CreateValidators() |
|||
{ |
|||
if (Properties.SchemaId != Guid.Empty) |
|||
{ |
|||
yield return new ReferencesValidator(Properties.IsRequired, Properties.SchemaId); |
|||
} |
|||
} |
|||
|
|||
public IEnumerable<Guid> GetReferencedIds(JToken value) |
|||
{ |
|||
Guid[] referenceIds; |
|||
try |
|||
{ |
|||
referenceIds = value?.ToObject<Guid[]>() ?? EmptyIds; |
|||
} |
|||
catch |
|||
{ |
|||
referenceIds = EmptyIds; |
|||
} |
|||
|
|||
return referenceIds.Union(new [] { Properties.SchemaId }); |
|||
} |
|||
|
|||
public JToken RemoveDeletedReferences(JToken value, ISet<Guid> deletedReferencedIds) |
|||
{ |
|||
if (value == null || value.Type == JTokenType.Null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (deletedReferencedIds.Contains(Properties.SchemaId)) |
|||
{ |
|||
return new JArray(); |
|||
} |
|||
|
|||
var oldReferenceIds = GetReferencedIds(value).TakeWhile(x => x != Properties.SchemaId).ToArray(); |
|||
var newReferenceIds = oldReferenceIds.Where(x => !deletedReferencedIds.Contains(x)).ToList(); |
|||
|
|||
return newReferenceIds.Count != oldReferenceIds.Length ? JToken.FromObject(newReferenceIds) : value; |
|||
} |
|||
|
|||
public override object ConvertValue(JToken value) |
|||
{ |
|||
return new ReferencesValue(value.ToObject<Guid[]>()); |
|||
} |
|||
|
|||
protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func<string, JsonSchema4, JsonSchema4> schemaResolver) |
|||
{ |
|||
var itemSchema = schemaResolver("ReferenceItem", new JsonSchema4 { Type = JsonObjectType.String }); |
|||
|
|||
jsonProperty.Type = JsonObjectType.Array; |
|||
jsonProperty.Item = itemSchema; |
|||
} |
|||
|
|||
protected override IEdmTypeReference CreateEdmType() |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
// ==========================================================================
|
|||
// ReferencesFieldProperties.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Core.Schemas |
|||
{ |
|||
[TypeName("References")] |
|||
public sealed class ReferencesFieldProperties : FieldProperties |
|||
{ |
|||
private Guid schemaId; |
|||
|
|||
public Guid SchemaId |
|||
{ |
|||
get { return schemaId; } |
|||
set |
|||
{ |
|||
ThrowIfFrozen(); |
|||
|
|||
schemaId = value; |
|||
} |
|||
} |
|||
|
|||
public override JToken GetDefaultValue() |
|||
{ |
|||
return new JArray(); |
|||
} |
|||
|
|||
protected override IEnumerable<ValidationError> ValidateCore() |
|||
{ |
|||
yield break; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
// ==========================================================================
|
|||
// ValidationContext.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Core.Schemas |
|||
{ |
|||
public sealed class ValidationContext |
|||
{ |
|||
private readonly Func<Guid, Guid, Task<bool>> checkContent; |
|||
private readonly Func<Guid, Task<bool>> checkAsset; |
|||
|
|||
public bool IsOptional { get; } |
|||
|
|||
public ValidationContext( |
|||
Func<Guid, Guid, Task<bool>> checkContent, |
|||
Func<Guid, Task<bool>> checkAsset) |
|||
: this(checkContent, checkAsset, false) |
|||
{ |
|||
|
|||
} |
|||
|
|||
private ValidationContext( |
|||
Func<Guid, Guid, Task<bool>> checkContent, |
|||
Func<Guid, Task<bool>> checkAsset, |
|||
bool isOptional) |
|||
{ |
|||
Guard.NotNull(checkAsset, nameof(checkAsset)); |
|||
Guard.NotNull(checkContent, nameof(checkAsset)); |
|||
|
|||
this.checkContent = checkContent; |
|||
this.checkAsset = checkAsset; |
|||
|
|||
IsOptional = isOptional; |
|||
} |
|||
|
|||
public ValidationContext Optional(bool isOptional) |
|||
{ |
|||
return isOptional == IsOptional ? this : new ValidationContext(checkContent, checkAsset, isOptional); |
|||
} |
|||
|
|||
public async Task<bool> IsValidContentIdAsync(Guid schemaId, Guid contentId) |
|||
{ |
|||
return contentId != Guid.Empty && schemaId != Guid.Empty && await checkContent(schemaId, contentId); |
|||
} |
|||
|
|||
public async Task<bool> IsValidAssetIdAsync(Guid assetId) |
|||
{ |
|||
return assetId != Guid.Empty && await checkAsset(assetId); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
// ==========================================================================
|
|||
// ReferencesValidator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Squidex.Core.Schemas.Validators |
|||
{ |
|||
public sealed class ReferencesValidator : IValidator |
|||
{ |
|||
private readonly bool isRequired; |
|||
private readonly Guid schemaId; |
|||
|
|||
public ReferencesValidator(bool isRequired, Guid schemaId) |
|||
{ |
|||
this.isRequired = isRequired; |
|||
this.schemaId = schemaId; |
|||
} |
|||
|
|||
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError) |
|||
{ |
|||
var references = value as ReferencesValue; |
|||
|
|||
if (references == null || references.ContentIds.Count == 0) |
|||
{ |
|||
if (isRequired && !context.IsOptional) |
|||
{ |
|||
addError("<FIELD> is required"); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
var referenceTasks = references.ContentIds.Select(x => CheckReferenceAsync(context, x)).ToArray(); |
|||
|
|||
await Task.WhenAll(referenceTasks); |
|||
|
|||
foreach (var notFoundId in referenceTasks.Where(x => !x.Result.IsFound).Select(x => x.Result.ReferenceId)) |
|||
{ |
|||
addError($"<FIELD> contains invalid reference '{notFoundId}'"); |
|||
} |
|||
} |
|||
|
|||
private async Task<(Guid ReferenceId, bool IsFound)> CheckReferenceAsync(ValidationContext context, Guid id) |
|||
{ |
|||
var isFound = await context.IsValidContentIdAsync(schemaId, id); |
|||
|
|||
return (id, isFound); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
// ==========================================================================
|
|||
// ReferencesFieldPropertiesDto.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using NJsonSchema.Annotations; |
|||
using Squidex.Core.Schemas; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
namespace Squidex.Controllers.Api.Schemas.Models |
|||
{ |
|||
[JsonSchema("References")] |
|||
public sealed class ReferencesFieldPropertiesDto : FieldPropertiesDto |
|||
{ |
|||
/// <summary>
|
|||
/// The id of the referenced schema.
|
|||
/// </summary>
|
|||
public Guid SchemaId { get; set; } |
|||
|
|||
public override FieldProperties ToProperties() |
|||
{ |
|||
return SimpleMapper.Map(this, new ReferencesFieldProperties()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
// ==========================================================================
|
|||
// ReferenceFieldPropertiesTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Core.Schemas |
|||
{ |
|||
public class ReferencesFieldPropertiesTests |
|||
{ |
|||
[Fact] |
|||
public void Should_set_or_freeze_sut() |
|||
{ |
|||
var sut = new ReferencesFieldProperties(); |
|||
|
|||
foreach (var property in sut.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) |
|||
{ |
|||
var value = |
|||
property.PropertyType.GetTypeInfo().IsValueType ? |
|||
Activator.CreateInstance(property.PropertyType) : |
|||
null; |
|||
|
|||
property.SetValue(sut, value); |
|||
|
|||
var result = property.GetValue(sut); |
|||
|
|||
Assert.Equal(value, result); |
|||
} |
|||
|
|||
sut.Freeze(); |
|||
|
|||
foreach (var property in sut.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) |
|||
{ |
|||
var value = |
|||
property.PropertyType.GetTypeInfo().IsValueType ? |
|||
Activator.CreateInstance(property.PropertyType) : |
|||
null; |
|||
|
|||
Assert.Throws<InvalidOperationException>(() => |
|||
{ |
|||
try |
|||
{ |
|||
property.SetValue(sut, value); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
throw ex.InnerException; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,208 @@ |
|||
// ==========================================================================
|
|||
// ReferenceFieldTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using FluentAssertions; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Infrastructure.Tasks; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Core.Schemas |
|||
{ |
|||
public class ReferencesFieldTests |
|||
{ |
|||
private readonly List<string> errors = new List<string>(); |
|||
private readonly Guid schemaId = Guid.NewGuid(); |
|||
private static readonly ValidationContext InvalidSchemaContext = new ValidationContext((x, y) => TaskHelper.False, x => TaskHelper.False); |
|||
|
|||
[Fact] |
|||
public void Should_instantiate_field() |
|||
{ |
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); |
|||
|
|||
Assert.Equal("my-refs", sut.Name); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_clone_object() |
|||
{ |
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); |
|||
|
|||
Assert.NotEqual(sut, sut.Enable()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_add_error_if_references_are_valid() |
|||
{ |
|||
var referenceId = Guid.NewGuid(); |
|||
|
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); |
|||
|
|||
await sut.ValidateAsync(CreateValue(referenceId), errors, InvalidSchemaContext); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_add_error_if_references_are_null_and_valid() |
|||
{ |
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); |
|||
|
|||
await sut.ValidateAsync(CreateValue(null), errors); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_errors_if_references_are_required_and_null() |
|||
{ |
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); |
|||
|
|||
await sut.ValidateAsync(CreateValue(null), errors); |
|||
|
|||
errors.ShouldBeEquivalentTo( |
|||
new[] { "<FIELD> is required" }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_errors_if_references_are_required_and_empty() |
|||
{ |
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); |
|||
|
|||
await sut.ValidateAsync(CreateValue(), errors); |
|||
|
|||
errors.ShouldBeEquivalentTo( |
|||
new[] { "<FIELD> is required" }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_errors_if_value_is_not_valid() |
|||
{ |
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); |
|||
|
|||
await sut.ValidateAsync("invalid", errors); |
|||
|
|||
errors.ShouldBeEquivalentTo( |
|||
new[] { "<FIELD> is not a valid value" }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_errors_if_reference_are_not_valid() |
|||
{ |
|||
var referenceId = Guid.NewGuid(); |
|||
|
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); |
|||
|
|||
await sut.ValidateAsync(CreateValue(referenceId), errors, InvalidSchemaContext); |
|||
|
|||
errors.ShouldBeEquivalentTo( |
|||
new[] { $"<FIELD> contains invalid reference '{referenceId}'" }); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_ids() |
|||
{ |
|||
var id1 = Guid.NewGuid(); |
|||
var id2 = Guid.NewGuid(); |
|||
|
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); |
|||
|
|||
var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); |
|||
|
|||
Assert.Equal(new[] { id1, id2, schemaId }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_list_with_schema_idempty_list_for_referenced_ids_when_null() |
|||
{ |
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); |
|||
|
|||
var result = sut.GetReferencedIds(null).ToArray(); |
|||
|
|||
Assert.Equal(new[] { schemaId }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_list_with_schema_id_for_referenced_ids_when_other_type() |
|||
{ |
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); |
|||
|
|||
var result = sut.GetReferencedIds("invalid").ToArray(); |
|||
|
|||
Assert.Equal(new[] { schemaId }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_null_when_removing_references_from_null_array() |
|||
{ |
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); |
|||
|
|||
var result = sut.RemoveDeletedReferences(null, null); |
|||
|
|||
Assert.Null(result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_null_when_removing_references_from_null_json_array() |
|||
{ |
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); |
|||
|
|||
var result = sut.RemoveDeletedReferences(JValue.CreateNull(), null); |
|||
|
|||
Assert.Null(result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_remove_deleted_references() |
|||
{ |
|||
var id1 = Guid.NewGuid(); |
|||
var id2 = Guid.NewGuid(); |
|||
|
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); |
|||
|
|||
var result = sut.RemoveDeletedReferences(CreateValue(id1, id2), new HashSet<Guid>(new[] { id2 })); |
|||
|
|||
Assert.Equal(CreateValue(id1), result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_remove_all_references_when_schema_is_removed() |
|||
{ |
|||
var id1 = Guid.NewGuid(); |
|||
var id2 = Guid.NewGuid(); |
|||
|
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); |
|||
|
|||
var result = sut.RemoveDeletedReferences(CreateValue(id1, id2), new HashSet<Guid>(new[] { schemaId })); |
|||
|
|||
Assert.Equal(CreateValue(), result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_same_token_when_removing_references_and_nothing_to_remove() |
|||
{ |
|||
var id1 = Guid.NewGuid(); |
|||
var id2 = Guid.NewGuid(); |
|||
|
|||
var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); |
|||
|
|||
var token = CreateValue(id1, id2); |
|||
var result = sut.RemoveDeletedReferences(token, new HashSet<Guid>(new[] { Guid.NewGuid() })); |
|||
|
|||
Assert.Same(token, result); |
|||
} |
|||
|
|||
private static JToken CreateValue(params Guid[] ids) |
|||
{ |
|||
return ids == null ? JValue.CreateNull() : (JToken)new JArray(ids.OfType<object>().ToArray()); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue