// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.EventSynchronization { public static class SchemaSynchronizer { public static IEnumerable Synchronize(this Schema source, Schema? target, Func idGenerator, SchemaSynchronizationOptions? options = null) { Guard.NotNull(source); Guard.NotNull(idGenerator); if (target == null) { yield return new SchemaDeleted(); } else { options ??= new SchemaSynchronizationOptions(); if (!source.Properties.Equals(target.Properties)) { yield return new SchemaUpdated { Properties = target.Properties }; } if (!source.Category.StringEquals(target.Category)) { yield return new SchemaCategoryChanged { Name = target.Category }; } if (!source.Scripts.Equals(target.Scripts)) { yield return new SchemaScriptsConfigured { Scripts = target.Scripts }; } if (!source.PreviewUrls.EqualsDictionary(target.PreviewUrls)) { yield return new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls }; } if (source.IsPublished != target.IsPublished) { yield return target.IsPublished ? new SchemaPublished() : new SchemaUnpublished(); } var events = SyncFields(source.FieldCollection, target.FieldCollection, idGenerator, CanUpdateRoot, options); foreach (var @event in events) { yield return @event; } if (!source.FieldsInLists.Equals(target.FieldsInLists)) { yield return new SchemaUIFieldsConfigured { FieldsInLists = target.FieldsInLists }; } if (!source.FieldsInReferences.Equals(target.FieldsInReferences)) { yield return new SchemaUIFieldsConfigured { FieldsInReferences = target.FieldsInReferences }; } if (!source.FieldRules.Equals(target.FieldRules)) { yield return new SchemaFieldRulesConfigured { FieldRules = target.FieldRules }; } } } private static IEnumerable SyncFields( FieldCollection source, FieldCollection target, Func idGenerator, Func canUpdate, SchemaSynchronizationOptions options) where T : class, IField { var sourceIds = source.Ordered.Select(x => x.NamedId()).ToList(); if (!options.NoFieldDeletion) { foreach (var sourceField in source.Ordered) { if (!target.ByName.TryGetValue(sourceField.Name, out _)) { var id = sourceField.NamedId(); sourceIds.Remove(id); yield return 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)) { canCreateField = false; id = sourceField.NamedId(); if (canUpdate(sourceField, targetField)) { if (!sourceField.RawProperties.Equals(targetField.RawProperties as object)) { yield return new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }; } } else if (!sourceField.IsLocked && !options.NoFieldRecreation) { canCreateField = true; sourceIds.Remove(id); yield return new FieldDeleted { FieldId = id }; } } 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, Partitioning = partitioning, Properties = targetField.RawProperties, FieldId = id }; sourceIds.Add(id); } if (id != null && (sourceField == null || CanUpdate(sourceField, targetField))) { if (!targetField.IsLocked.BoolEquals(sourceField?.IsLocked)) { yield return new FieldLocked { FieldId = id }; } if (!targetField.IsHidden.BoolEquals(sourceField?.IsHidden)) { yield return targetField.IsHidden ? new FieldHidden { FieldId = id } : new FieldShown { FieldId = id }; } if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled)) { yield return targetField.IsDisabled ? new FieldDisabled { FieldId = id } : new FieldEnabled { FieldId = id }; } if (sourceField is null or IArrayField && targetField is IArrayField targetArrayField) { var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection.Empty; var events = SyncFields(fields, targetArrayField.FieldCollection, idGenerator, CanUpdate, options); foreach (var @event in events) { @event.ParentFieldId = id; yield return @event; } } } } if (sourceIds.Count > 1) { var sourceNames = sourceIds.Select(x => x.Name).ToHashSet(); var targetNames = target.Ordered.Select(x => x.Name).ToHashSet(); if (sourceNames.SetEquals(targetNames) && !sourceNames.SequenceEqual(targetNames)) { var fieldIds = targetNames.Select(x => sourceIds.Find(y => y.Name == x)!.Id).ToArray(); yield return new SchemaFieldsReordered { FieldIds = fieldIds }; } } } private static bool CanUpdateRoot(IRootField source, IRootField target) { return CanUpdate(source, target) && source.Partitioning == target.Partitioning; } private static bool CanUpdate(IField source, IField target) { return !source.IsLocked && source.Name == target.Name && source.RawProperties.TypeEquals(target.RawProperties); } } }