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