From 61d82b042f748b647f965a5f19e12963660dcc3a Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 26 Jan 2019 19:38:24 +0100 Subject: [PATCH] Schema sync. --- .../Schemas/ArrayField.cs | 5 + .../Schemas/Fields.cs | 2 +- .../Schemas/IArrayField.cs | 2 + .../Schemas/Schema.cs | 76 ++- .../SchemaSynchronizationOptions.cs | 16 + .../SchemaSynchronizer.cs | 208 ++++++++ .../EventSynchronization/SyncHelpers.cs | 46 ++ .../Schemas/SchemaScriptsConfigured.cs | 18 + .../EventSynchronization/AssertHelper.cs | 51 ++ .../SchemaSynchronizerTests.cs | 505 ++++++++++++++++++ 10 files changed, 920 insertions(+), 9 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs create mode 100644 src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs index 1de894b81..c58263cb1 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs @@ -31,6 +31,11 @@ namespace Squidex.Domain.Apps.Core.Schemas get { return fields.ByName; } } + public FieldCollection FieldCollection + { + get { return fields; } + } + public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null, IFieldSettings settings = null) : base(id, name, partitioning, properties) { diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs index 570e75aa5..2ae68dabe 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs @@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Core.Schemas return new NestedField(id, name, properties, settings); } - public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func handler, ArrayFieldProperties properties = null, IFieldSettings settings = null) + public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func handler = null, ArrayFieldProperties properties = null, IFieldSettings settings = null) { var field = Array(id, name, partitioning, properties, settings); diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs index 0ea74cfbd..825013f46 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs @@ -16,5 +16,7 @@ namespace Squidex.Domain.Apps.Core.Schemas IReadOnlyDictionary FieldsById { get; } IReadOnlyDictionary FieldsByName { get; } + + FieldCollection FieldCollection { get; } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs index c6e187087..717bec234 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs @@ -14,8 +14,14 @@ namespace Squidex.Domain.Apps.Core.Schemas { public sealed class Schema : Cloneable { + private static readonly Dictionary EmptyScripts = new Dictionary(); + private static readonly Dictionary EmptyPreviewUrls = new Dictionary(); private readonly string name; + private readonly bool isSingleton; + private string category; private FieldCollection fields = FieldCollection.Empty; + private IReadOnlyDictionary scripts = EmptyScripts; + private IReadOnlyDictionary previewUrls = EmptyPreviewUrls; private SchemaProperties properties; private bool isPublished; @@ -24,11 +30,21 @@ namespace Squidex.Domain.Apps.Core.Schemas get { return name; } } + public string Category + { + get { return category; } + } + public bool IsPublished { get { return isPublished; } } + public bool IsSingleton + { + get { return isSingleton; } + } + public IReadOnlyList Fields { get { return fields.Ordered; } @@ -44,12 +60,27 @@ namespace Squidex.Domain.Apps.Core.Schemas get { return fields.ByName; } } + public IReadOnlyDictionary Scripts + { + get { return scripts; } + } + + public IReadOnlyDictionary PreviewUrls + { + get { return previewUrls; } + } + + public FieldCollection FieldCollection + { + get { return fields; } + } + public SchemaProperties Properties { get { return properties; } } - public Schema(string name, SchemaProperties properties = null) + public Schema(string name, SchemaProperties properties = null, bool isSingleton = false) { Guard.NotNullOrEmpty(name, nameof(name)); @@ -57,10 +88,12 @@ namespace Squidex.Domain.Apps.Core.Schemas this.properties = properties ?? new SchemaProperties(); this.properties.Freeze(); + + this.isSingleton = isSingleton; } - public Schema(string name, RootField[] fields, SchemaProperties properties, bool isPublished) - : this(name, properties) + public Schema(string name, RootField[] fields, SchemaProperties properties, bool isPublished, bool isSingleton = false) + : this(name, properties, isSingleton) { Guard.NotNull(fields, nameof(fields)); @@ -99,31 +132,58 @@ namespace Squidex.Domain.Apps.Core.Schemas }); } + [Pure] + public Schema MoveTo(string category) + { + return Clone(clone => + { + clone.category = category; + }); + } + + [Pure] + public Schema ConfigureScripts(IReadOnlyDictionary scripts) + { + return Clone(clone => + { + clone.scripts = scripts ?? EmptyScripts; + }); + } + + [Pure] + public Schema ConfigurePreviewUrls(IReadOnlyDictionary previewUrls) + { + return Clone(clone => + { + clone.previewUrls = previewUrls ?? EmptyPreviewUrls; + }); + } + [Pure] public Schema DeleteField(long fieldId) { - return Updatefields(f => f.Remove(fieldId)); + return UpdateFields(f => f.Remove(fieldId)); } [Pure] public Schema ReorderFields(List ids) { - return Updatefields(f => f.Reorder(ids)); + return UpdateFields(f => f.Reorder(ids)); } [Pure] public Schema AddField(RootField field) { - return Updatefields(f => f.Add(field)); + return UpdateFields(f => f.Add(field)); } [Pure] public Schema UpdateField(long fieldId, Func updater) { - return Updatefields(f => f.Update(fieldId, updater)); + return UpdateFields(f => f.Update(fieldId, updater)); } - private Schema Updatefields(Func, FieldCollection> updater) + private Schema UpdateFields(Func, FieldCollection> updater) { var newFields = updater(fields); diff --git a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs b/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs new file mode 100644 index 000000000..35588d0b4 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.EventSynchronization +{ + public sealed class SchemaSynchronizationOptions + { + public bool NoFieldDeletion { get; set; } + + public bool NoFieldRecreation { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs new file mode 100644 index 000000000..5622b4807 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs @@ -0,0 +1,208 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.EventSynchronization +{ + public static class SchemaSynchronizer + { + public static IEnumerable Synchronize(this Schema source, Schema target, IJsonSerializer serializer, Func idGenerator, SchemaSynchronizationOptions options = null) + { + Guard.NotNull(source, nameof(source)); + Guard.NotNull(serializer, nameof(serializer)); + Guard.NotNull(idGenerator, nameof(idGenerator)); + + if (target == null) + { + yield return new SchemaDeleted(); + } + else + { + options = options ?? new SchemaSynchronizationOptions(); + + SchemaEvent E(SchemaEvent @event) + { + return @event; + } + + var events = SyncFields(source.FieldCollection, target.FieldCollection, serializer, idGenerator, null, options); + + foreach (var @event in events) + { + yield return E(@event); + } + + if (!source.Properties.EqualsJson(target.Properties, serializer)) + { + yield return E(new SchemaUpdated { Properties = target.Properties }); + } + + if (!source.Category.StringEquals(target.Category)) + { + yield return E(new SchemaCategoryChanged { Name = target.Category }); + } + + if (!source.PreviewUrls.EqualsDictionary(target.PreviewUrls)) + { + yield return E(new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls.ToDictionary(x => x.Key, x => x.Value) }); + } + + if (!source.Scripts.EqualsDictionary(target.Scripts)) + { + yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts.ToDictionary(x => x.Key, x => x.Value) }); + } + + if (source.IsPublished != target.IsPublished) + { + yield return target.IsPublished ? + E(new SchemaPublished()) : + E(new SchemaUnpublished()); + } + } + } + + private static IEnumerable SyncFields( + FieldCollection source, + FieldCollection target, + IJsonSerializer serializer, + Func idGenerator, + NamedId parentId, SchemaSynchronizationOptions options) where T : class, IField + { + FieldEvent E(FieldEvent @event) + { + @event.ParentFieldId = parentId; + + return @event; + } + + var sourceIds = new List>(source.Ordered.Select(x => x.NamedId())); + var sourceNames = sourceIds.Select(x => x.Name).ToList(); + + if (!options.NoFieldDeletion) + { + foreach (var sourceField in source.Ordered) + { + if (!target.ByName.TryGetValue(sourceField.Name, out var targetField)) + { + var id = sourceField.NamedId(); + + sourceIds.Remove(id); + sourceNames.Remove(id.Name); + + yield return E(new FieldDeleted { FieldId = id }); + } + } + } + + foreach (var targetField in target.Ordered) + { + NamedId id = null; + + var canCreateField = true; + + if (source.ByName.TryGetValue(targetField.Name, out var sourceField)) + { + id = sourceField.NamedId(); + + if (CanUpdate(sourceField, targetField)) + { + if (!sourceField.RawProperties.EqualsJson(targetField.RawProperties, serializer)) + { + yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }); + } + } + else if (!sourceField.IsLocked && !options.NoFieldRecreation) + { + yield return E(new FieldDeleted { FieldId = id }); + } + else + { + canCreateField = false; + } + } + else if (canCreateField) + { + var partitioning = (string)null; + + if (targetField is IRootField rootField) + { + partitioning = rootField.Partitioning.Key; + } + + id = NamedId.Of(idGenerator(), targetField.Name); + + yield return new FieldAdded + { + Name = targetField.Name, + ParentFieldId = parentId, + Partitioning = partitioning, + Properties = targetField.RawProperties, + FieldId = id + }; + + sourceIds.Add(id); + sourceNames.Add(id.Name); + } + + if (id != null && (sourceField == null || CanUpdate(sourceField, targetField))) + { + if (!targetField.IsLocked.BoolEquals(sourceField?.IsLocked)) + { + yield return E(new FieldLocked { FieldId = id }); + } + + if (!targetField.IsHidden.BoolEquals(sourceField?.IsHidden)) + { + yield return targetField.IsHidden ? + E(new FieldHidden { FieldId = id }) : + E(new FieldShown { FieldId = id }); + } + + if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled)) + { + yield return targetField.IsDisabled ? + E(new FieldDisabled { FieldId = id }) : + E(new FieldEnabled { FieldId = id }); + } + + if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField) + { + var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection.Empty; + + var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, id, options); + + foreach (var @event in events) + { + yield return @event; + } + } + } + } + + var targetNames = target.Ordered.Select(x => x.Name); + + if (sourceNames.Intersect(targetNames).Count() == target.Ordered.Count && !sourceNames.SequenceEqual(targetNames)) + { + yield return new SchemaFieldsReordered { FieldIds = sourceIds.Select(x => x.Id).ToList(), ParentFieldId = parentId }; + } + } + + private static bool CanUpdate(IField source, IField target) + { + return !source.IsLocked && source.Name == target.Name && source.RawProperties.TypeEquals(target.RawProperties); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs b/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs new file mode 100644 index 000000000..827ccb744 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using NamedIdStatic = Squidex.Infrastructure.NamedId; + +namespace Squidex.Domain.Apps.Core.EventSynchronization +{ + public static class SyncHelpers + { + public static bool BoolEquals(this bool lhs, bool? rhs) + { + return lhs == (rhs ?? false); + } + + public static bool StringEquals(this string lhs, string rhs) + { + return string.Equals(lhs ?? string.Empty, rhs ?? string.Empty, StringComparison.Ordinal); + } + + public static bool TypeEquals(this object lhs, object rhs) + { + return lhs.GetType() == rhs.GetType(); + } + + public static NamedId NamedId(this IField field) + { + return NamedIdStatic.Of(field.Id, field.Name); + } + + public static bool EqualsJson(this T lhs, T rhs, IJsonSerializer serializer) + { + var lhsJson = serializer.Serialize(lhs); + var rhsJson = serializer.Serialize(rhs); + + return string.Equals(lhsJson, rhsJson); + } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs b/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs new file mode 100644 index 000000000..1deeabd55 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Schemas +{ + [EventType(nameof(SchemaScriptsConfigured))] + public sealed class SchemaScriptsConfigured : SchemaEvent + { + public Dictionary Scripts { get; set; } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs new file mode 100644 index 000000000..073a73677 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using FluentAssertions.Equivalency; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization +{ + public static class AssertHelper + { + public static void ShouldHaveSameEvents(this IEnumerable events, params IEvent[] others) + { + var source = events.ToArray(); + + source.Should().HaveSameCount(others); + + for (var i = 0; i < source.Length; i++) + { + var lhs = source[i]; + var rhs = others[i]; + + lhs.ShouldBeSameEvent(rhs); + } + } + + public static void ShouldBeSameEvent(this IEvent lhs, IEvent rhs) + { + lhs.Should().BeOfType(rhs.GetType()); + + ((object)lhs).Should().BeEquivalentTo(rhs, o => o.IncludingAllRuntimeProperties().Excluding((IMemberInfo x) => x.SelectedMemberPath == "Properties.IsFrozen")); + } + + public static void ShouldBeSameEventType(this IEvent lhs, IEvent rhs) + { + lhs.Should().BeOfType(rhs.GetType()); + } + + public static void ShouldBeEquivalent(this J lhs, T rhs) + { + lhs.Value.Should().BeEquivalentTo(rhs, o => o.IncludingProperties()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs new file mode 100644 index 000000000..e6394ccb7 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs @@ -0,0 +1,505 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.EventSynchronization; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization +{ + public class SchemaSynchronizerTests + { + private readonly Func idGenerator; + private readonly IJsonSerializer jsonSerializer = TestUtils.DefaultSerializer; + private readonly NamedId stringId = NamedId.Of(13L, "my-value"); + private readonly NamedId nestedId = NamedId.Of(141L, "my-value"); + private readonly NamedId arrayId = NamedId.Of(14L, "11-array"); + private int fields = 50; + + public SchemaSynchronizerTests() + { + idGenerator = () => fields++; + } + + [Fact] + public void Should_create_events_if_schema_deleted() + { + var sourceSchema = new Schema("source"); + var targetSchema = (Schema)null; + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaDeleted() + ); + } + + [Fact] + public void Should_create_events_if_category_changed() + { + var sourceSchema = new Schema("source"); + var targetSchema = new Schema("target").MoveTo("Category"); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaCategoryChanged { Name = "Category" } + ); + } + + [Fact] + public void Should_create_events_if_scripts_configured() + { + var scripts = new Dictionary + { + ["Create"] = "Script" + }; + + var sourceSchema = new Schema("source"); + var targetSchema = new Schema("target").ConfigureScripts(scripts); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaScriptsConfigured { Scripts = scripts } + ); + } + + [Fact] + public void Should_create_events_if_preview_urls_configured() + { + var previewUrls = new Dictionary + { + ["Web"] = "Url" + }; + + var sourceSchema = new Schema("source"); + var targetSchema = new Schema("target").ConfigurePreviewUrls(previewUrls); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaPreviewUrlsConfigured { PreviewUrls = previewUrls } + ); + } + + [Fact] + public void Should_create_events_if_schema_published() + { + var sourceSchema = new Schema("source"); + var targetSchema = new Schema("target").Publish(); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaPublished() + ); + } + + [Fact] + public void Should_create_events_if_schema_unpublished() + { + var sourceSchema = new Schema("source").Publish(); + var targetSchema = new Schema("target"); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaUnpublished() + ); + } + + [Fact] + public void Should_create_events_if_nested_field_deleted() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_deleted() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target"); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_updated() + { + var properties = new StringFieldProperties { IsRequired = true }; + + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name, properties)); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldUpdated { Properties = properties, FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_updated() + { + var properties = new StringFieldProperties { IsRequired = true }; + + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant, properties); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldUpdated { Properties = properties, FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_locked() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)).LockField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldLocked { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_locked() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant).LockField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldLocked { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_hidden() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)).HideField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldHidden { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_hidden() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant).HideField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldHidden { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_shown() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)).HideField(nestedId.Id, arrayId.Id); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldShown { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_shown() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant).HideField(stringId.Id); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldShown { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_disabled() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)).DisableField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDisabled { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_disabled() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant).DisableField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDisabled { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_enabled() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)).DisableField(nestedId.Id, arrayId.Id); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldEnabled { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_enabled() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant).DisableField(stringId.Id); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldEnabled { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_field_created() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant).HideField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + var createdId = NamedId.Of(50L, stringId.Name); + + events.ShouldHaveSameEvents( + new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new FieldHidden { FieldId = createdId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_created() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)).HideField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + var id1 = NamedId.Of(50L, arrayId.Name); + var id2 = NamedId.Of(51L, stringId.Name); + + events.ShouldHaveSameEvents( + new FieldAdded { FieldId = id1, Name = arrayId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new ArrayFieldProperties() }, + new FieldAdded { FieldId = id2, Name = stringId.Name, ParentFieldId = id1, Properties = new StringFieldProperties() }, + new FieldHidden { FieldId = id2, ParentFieldId = id1 } + ); + } + + [Fact] + public void Should_create_events_if_nested_fields_reordered() + { + var id1 = NamedId.Of(1, "f1"); + var id2 = NamedId.Of(2, "f1"); + + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(10, "f1") + .AddString(11, "f2")); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(20, "f2") + .AddString(15, "f1")); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaFieldsReordered { FieldIds = new List { 11, 10 }, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_fields_reordered() + { + var id1 = NamedId.Of(1, "f1"); + var id2 = NamedId.Of(2, "f1"); + + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(20, "f2", Partitioning.Invariant) + .AddString(15, "f1", Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaFieldsReordered { FieldIds = new List { 11, 10 } } + ); + } + + [Fact] + public void Should_create_events_if_fields_reordered_after_sync() + { + var id1 = NamedId.Of(1, "f1"); + var id2 = NamedId.Of(2, "f1"); + + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(25, "f3", Partitioning.Invariant) + .AddString(20, "f1", Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = NamedId.Of(11L, "f2") }, + new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new SchemaFieldsReordered { FieldIds = new List { 50, 10 } } + ); + } + } +}