diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2d7de4ce..ec2e72a5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,13 +1,16 @@ --- name: Bug report about: Create a report to help us improve - --- ### Brief bug description What went wrong? +## Output logs + +Please include the command line output log file and migration protocol generated for your `Migration.Tool.CLI.exe migrate` command. + ### Repro steps 1. Go to '...' @@ -21,9 +24,9 @@ What is the correct behavior? ### Test environment - - Platform/OS: [e.g. .NET Core 2.1, iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] +- Platform/OS: [e.g. .NET Core 2.1, iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] ### Additional context diff --git a/KVA/Migration.Tool.Source/Behaviors/XbKApiContextBehavior.cs b/KVA/Migration.Tool.Source/Behaviors/XbyKApiContextBehavior.cs similarity index 69% rename from KVA/Migration.Tool.Source/Behaviors/XbKApiContextBehavior.cs rename to KVA/Migration.Tool.Source/Behaviors/XbyKApiContextBehavior.cs index 765129ca..436f1ce5 100644 --- a/KVA/Migration.Tool.Source/Behaviors/XbKApiContextBehavior.cs +++ b/KVA/Migration.Tool.Source/Behaviors/XbyKApiContextBehavior.cs @@ -11,8 +11,8 @@ namespace Migration.Tool.Source.Behaviors; -public class XbKApiContextBehavior( - ILogger> logger, +public class XbyKApiContextBehavior( + ILogger> logger, IMigrationProtocol protocol, KxpApiInitializer initializer) : IPipelineBehavior @@ -28,9 +28,9 @@ public async Task Handle(TRequest request, RequestHandlerDelegate() - .WithMessage($"Target XbK doesn't contain default administrator account ('{UserInfoProvider.DEFAULT_ADMIN_USERNAME}'). Default administrator account is required for migration.") + .WithMessage($"Target XbyK doesn't contain default administrator account ('{UserInfoProvider.DEFAULT_ADMIN_USERNAME}'). Default administrator account is required for migration.") ); - throw new InvalidOperationException($"Target XbK doesn't contain default administrator account ('{UserInfoProvider.DEFAULT_ADMIN_USERNAME}')"); + throw new InvalidOperationException($"Target XbyK doesn't contain default administrator account ('{UserInfoProvider.DEFAULT_ADMIN_USERNAME}')"); } using (new CMSActionContext(defaultAdmin) { User = defaultAdmin, UseGlobalAdminContext = true }) diff --git a/KVA/Migration.Tool.Source/Contexts/SourceObjectContext.cs b/KVA/Migration.Tool.Source/Contexts/SourceObjectContext.cs index ee6d9ca7..a96154eb 100644 --- a/KVA/Migration.Tool.Source/Contexts/SourceObjectContext.cs +++ b/KVA/Migration.Tool.Source/Contexts/SourceObjectContext.cs @@ -5,3 +5,5 @@ namespace Migration.Tool.Source.Contexts; public record DocumentSourceObjectContext(ICmsTree CmsTree, ICmsClass NodeClass, ICmsSite Site, FormInfo OldFormInfo, FormInfo NewFormInfo, int? DocumentId) : ISourceObjectContext; + +public record CustomTableSourceObjectContext : ISourceObjectContext; diff --git a/KVA/Migration.Tool.Source/Handlers/MigrateCategoriesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigrateCategoriesCommandHandler.cs index b7fd93b6..1e8468bd 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigrateCategoriesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigrateCategoriesCommandHandler.cs @@ -1,5 +1,5 @@ using System.Collections; - +using System.Diagnostics; using CMS.ContentEngine; using CMS.ContentEngine.Internal; using CMS.DataEngine; @@ -12,12 +12,14 @@ using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; - using Migration.Tool.Common; using Migration.Tool.Common.Abstractions; +using Migration.Tool.Common.Builders; using Migration.Tool.Common.Helpers; +using Migration.Tool.KXP.Api; using Migration.Tool.Source.Mappers; using Migration.Tool.Source.Model; +using Migration.Tool.Source.Providers; using Migration.Tool.Source.Services; using Newtonsoft.Json; @@ -30,7 +32,9 @@ public class MigrateCategoriesCommandHandler( IImporter importer, ReusableSchemaService reusableSchemaService, IUmtMapper tagModelMapper, - SpoiledGuidContext spoiledGuidContext + SpoiledGuidContext spoiledGuidContext, + KxpClassFacade kxpClassFacade, + ClassMappingProvider classMappingProvider ) : IRequestHandler { public async Task Handle(MigrateCategoriesCommand request, CancellationToken cancellationToken) @@ -48,26 +52,42 @@ public async Task Handle(MigrateCategoriesCommand request, Cancel if (result.Imported is TaxonomyInfo taxonomy) { string query = """ - SELECT C.ClassName, C.ClassGuid, C.ClassID + SELECT C.ClassName, C.ClassGuid, C.ClassID, CC.CategoryID FROM View_CMS_Tree_Joined [TJ] JOIN dbo.CMS_DocumentCategory [CDC] on [TJ].DocumentID = [CDC].DocumentID JOIN CMS_Class [C] ON TJ.NodeClassID = [C].ClassID JOIN dbo.CMS_Category CC on CDC.CategoryID = CC.CategoryID AND CC.CategoryUserID IS NULL - GROUP BY C.ClassName, C.ClassGuid, C.ClassID + GROUP BY C.ClassName, C.ClassGuid, C.ClassID, CC.CategoryID """; - var classesWithCategories = modelFacade.Select(query, (reader, version) => new { ClassName = reader.Unbox("ClassName"), ClassGuid = reader.Unbox("ClassGuid"), ClassID = reader.Unbox("ClassID") }); + var classesWithCategories = modelFacade.Select(query, (reader, version) => new { ClassName = reader.Unbox("ClassName"), ClassGuid = reader.Unbox("ClassGuid"), ClassID = reader.Unbox("ClassID"), CategoryID = reader.Unbox("CategoryID") }) + .GroupBy(x => x.ClassGuid) + .Select(x => new { ClassGuid = x.Key, x.First().ClassName, x.First().ClassID, Categories = x.Select(row => row.CategoryID) }); + // For each source instance class whose documents have some categories assigned, include taxonomy-storage reusable schema in the target class + #region Ensure reusable schema var skippedClasses = new List(); var schemaGuid = Guid.Empty; string categoryFieldName = "Category_Legacy"; foreach (var classWithCategoryUsage in classesWithCategories) { var targetDataClass = DataClassInfoProvider.ProviderObject.Get(classWithCategoryUsage.ClassGuid); - if (targetDataClass == null) + if (targetDataClass is null) { - skippedClasses.Add(classWithCategoryUsage.ClassID); - logger.LogWarning("Data class not found by ClassGuid {Guid}", classWithCategoryUsage.ClassGuid); + // No direct-mapped target class found. Try to identify custom-mapped target class + var classMapping = classMappingProvider.GetMapping(classWithCategoryUsage.ClassName); + if (classMapping is not null) + { + if (classWithCategoryUsage.Categories.Any(cat => classMapping.IsCategoryMapped(classWithCategoryUsage.ClassName, cat))) + { + targetDataClass = kxpClassFacade.GetClass(classMapping.TargetClassName); + } + } + } + + if (targetDataClass is null) + { + logger.LogWarning($"Class(ClassGuid {{Guid}}) has documents with categories, but no directly-mapped data class nor custom-mapped class that would receive the categories (declared via {nameof(IClassMapping.IsCategoryMapped)}) was found", classWithCategoryUsage.ClassGuid); continue; } @@ -82,6 +102,8 @@ FROM View_CMS_Tree_Joined [TJ] DataClassInfoProvider.SetDataClassInfo(targetDataClass); } } + #endregion + var categories = modelFacade.Select( "CategoryEnabled = 1 AND CategoryUserID IS NULL", @@ -95,23 +117,24 @@ FROM View_CMS_Tree_Joined [TJ] categoryId2Guid.Add(cmsCategory.CategoryID, cmsCategory.CategoryGUID); // CategorySiteID - not migrated, Taxonomies are global! - var mapped = tagModelMapper.Map(new TagModelSource( + var tagUMTModels = tagModelMapper.Map(new TagModelSource( taxonomy.TaxonomyGUID, cmsCategory, categoryId2Guid )); - foreach (var umtModel in mapped) + foreach (var tagUMTModel in tagUMTModels) { if (await importer - .ImportAsync(umtModel) + .ImportAsync(tagUMTModel) .AssertSuccess(logger) is { Success: true, Info: { } tag }) { query = """ - SELECT TJ.DocumentGUID, TJ.NodeSiteID, TJ.NodeID, TJ.DocumentID, CDC.CategoryID, TJ.DocumentCheckedOutVersionHistoryID, TJ.NodeClassID + SELECT TJ.DocumentGUID, TJ.NodeSiteID, TJ.NodeID, TJ.DocumentID, CDC.CategoryID, TJ.DocumentCheckedOutVersionHistoryID, TJ.NodeClassID, C.ClassName FROM View_CMS_Tree_Joined [TJ] JOIN dbo.CMS_DocumentCategory [CDC] on [TJ].DocumentID = [CDC].DocumentID JOIN dbo.CMS_Category CC on CDC.CategoryID = CC.CategoryID AND CC.CategoryUserID IS NULL + JOIN CMS_Class [C] ON TJ.NodeClassID = [C].ClassID WHERE CDC.CategoryID = @categoryId """; @@ -120,6 +143,7 @@ FROM View_CMS_Tree_Joined [TJ] CategoryID = reader.Unbox("CategoryID"), DocumentCheckedOutVersionHistoryID = reader.Unbox("DocumentCheckedOutVersionHistoryID"), NodeClassID = reader.Unbox("NodeClassID"), + NodeClassName = reader.Unbox("ClassName"), NodeSiteID = reader.Unbox("NodeSiteID"), DocumentGUID = spoiledGuidContext.EnsureDocumentGuid( reader.Unbox("DocumentGUID"), @@ -137,13 +161,23 @@ FROM View_CMS_Tree_Joined [TJ] continue; } + var classMapping = classMappingProvider.GetMapping(dwc.NodeClassName); + if (classMapping is not null) + { + Debug.Assert(dwc.CategoryID.HasValue, "dwc.CategoryID should have value, otherwise the row would not be included in the query due to inner join"); + if (!classMapping.IsCategoryMapped(dwc.NodeClassName, dwc.CategoryID.Value)) + { + continue; + } + } + var commonData = ContentItemCommonDataInfo.Provider.Get() .WhereEquals(nameof(ContentItemCommonDataInfo.ContentItemCommonDataGUID), dwc.DocumentGUID) .FirstOrDefault(); if (commonData is null) { - logger.LogWarning("ContentItemCommonDataInfo not found by guid {Guid}, taxonomy cannot be migrated", dwc.DocumentGUID); + logger.LogWarning("ContentItemCommonDataInfo not found by Guid {Guid}. Taxonomy cannot be migrated", dwc.DocumentGUID); continue; } diff --git a/KVA/Migration.Tool.Source/Handlers/MigrateCustomModulesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigrateCustomModulesCommandHandler.cs index aeea5841..9190fce5 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigrateCustomModulesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigrateCustomModulesCommandHandler.cs @@ -173,7 +173,7 @@ private async Task MigrateClasses(EntityConfiguration entityConfiguration, Cance } } - // special case - member migration (CMS_User splits into CMS_User and CMS_Member in XbK) + // special case - member migration (CMS_User splits into CMS_User and CMS_Member in XbyK) await MigrateMemberClass(cancellationToken); } diff --git a/KVA/Migration.Tool.Source/Handlers/MigrateCustomTablesHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigrateCustomTablesHandler.cs index c14ede44..e5f2f303 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigrateCustomTablesHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigrateCustomTablesHandler.cs @@ -1,23 +1,27 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Xml.Linq; - +using CMS.ContentEngine.Internal; using CMS.DataEngine; using CMS.Modules; - +using Kentico.Xperience.UMT.Services; using MediatR; using Microsoft.Extensions.Logging; using Migration.Tool.Common; using Migration.Tool.Common.Abstractions; +using Migration.Tool.Common.Builders; using Migration.Tool.Common.Helpers; using Migration.Tool.Common.MigrationProtocol; using Migration.Tool.Common.Services.BulkCopy; using Migration.Tool.KXP.Api; using Migration.Tool.Source.Contexts; using Migration.Tool.Source.Helpers; +using Migration.Tool.Source.Mappers; using Migration.Tool.Source.Model; +using Migration.Tool.Source.Providers; +using Newtonsoft.Json; namespace Migration.Tool.Source.Handlers; @@ -27,8 +31,12 @@ public class MigrateCustomTablesHandler( KxpClassFacade kxpClassFacade, IProtocol protocol, BulkDataCopyService bulkDataCopyService, + IImporter importer, + IUmtMapper mapper, IEntityMapper dataClassMapper, - PrimaryKeyMappingContext primaryKeyMappingContext + PrimaryKeyMappingContext primaryKeyMappingContext, + ClassMappingProvider classMappingProvider, + ToolConfiguration configuration // ReusableSchemaService reusableSchemaService ) : IRequestHandler @@ -77,25 +85,34 @@ private async Task MigrateCustomTables() modelFacade.Select("ClassIsCustomTable=1", "ClassID ASC") ); + var manualMappings = classMappingProvider.ExecuteMappings(); + var remapped = new List<(ICmsClass ksClass, DataClassInfo target, IClassMapping mapping)>(); + while (srcClassesDe.GetNext(out var di)) { - var (_, srcClass) = di; + var (_, ksClass) = di; + + if (manualMappings.TryGetValue(ksClass.ClassName, out var mappedToClass)) + { + remapped.Add((ksClass, mappedToClass.target, mappedToClass.mappping)); + continue; + } - if (!srcClass.ClassIsCustomTable) + if (!ksClass.ClassIsCustomTable) { continue; } - if (srcClass.ClassInheritsFromClassID is { } classInheritsFromClassId && !primaryKeyMappingContext.HasMapping(c => c.ClassID, classInheritsFromClassId)) + if (ksClass.ClassInheritsFromClassID is { } classInheritsFromClassId && !primaryKeyMappingContext.HasMapping(c => c.ClassID, classInheritsFromClassId)) { // defer migration to later stage if (srcClassesDe.TryDeferItem(di)) { - logger.LogTrace("Class {Class} inheritance parent not found, deferring migration to end. Attempt {Attempt}", Printer.GetEntityIdentityPrint(srcClass), di.Recurrence); + logger.LogTrace("Class {Class} inheritance parent not found, deferring migration to end. Attempt {Attempt}", Printer.GetEntityIdentityPrint(ksClass), di.Recurrence); } else { - logger.LogErrorMissingDependency(srcClass, nameof(srcClass.ClassInheritsFromClassID), srcClass.ClassInheritsFromClassID, typeof(DataClassInfo)); + logger.LogErrorMissingDependency(ksClass, nameof(ksClass.ClassInheritsFromClassID), ksClass.ClassInheritsFromClassID, typeof(DataClassInfo)); protocol.Append(HandbookReferences .MissingRequiredDependency(nameof(ICmsClass.ClassID), classInheritsFromClassId) .NeedsManualAction() @@ -105,13 +122,13 @@ private async Task MigrateCustomTables() continue; } - protocol.FetchedSource(srcClass); + protocol.FetchedSource(ksClass); - var xbkDataClass = kxpClassFacade.GetClass(srcClass.ClassGUID); + var xbkDataClass = kxpClassFacade.GetClass(ksClass.ClassGUID); protocol.FetchedTarget(xbkDataClass); - if (await SaveClassUsingKxoApi(srcClass, xbkDataClass) is { } savedDataClass) + if (await SaveClassUsingKxoApi(ksClass, xbkDataClass) is { } savedDataClass) { Debug.Assert(savedDataClass.ClassID != 0, "xbkDataClass.ClassID != 0"); // MigrateClassSiteMappings(kx13Class, xbkDataClass); @@ -121,7 +138,7 @@ private async Task MigrateCustomTables() #region Migrate coupled data class data - if (srcClass.ClassShowAsSystemTable is false) + if (ksClass.ClassShowAsSystemTable is false) { Debug.Assert(xbkDataClass.ClassTableName != null, "kx13Class.ClassTableName != null"); // var csi = new ClassStructureInfo(kx13Class.ClassXmlSchema, kx13Class.ClassXmlSchema, kx13Class.ClassTableName); @@ -135,7 +152,7 @@ private async Task MigrateCustomTables() Debug.Assert(autoIncrementColumns.Count == 1, "autoIncrementColumns.Count == 1"); var r = (xbkDataClass.ClassTableName, xbkDataClass.ClassGUID, autoIncrementColumns); - logger.LogTrace("Class '{ClassGuild}' Resolved as: {Result}", srcClass.ClassGUID, r); + logger.LogTrace("Class '{ClassGuild}' Resolved as: {Result}", ksClass.ClassGUID, r); try { @@ -166,6 +183,60 @@ private async Task MigrateCustomTables() #endregion } } + + foreach (var (cmsClass, targetDataClass, mapping) in remapped) + { + if (string.IsNullOrWhiteSpace(cmsClass.ClassTableName)) + { + logger.LogError("Class {Class} is missing table name", cmsClass.ClassName); + continue; + } + + var customTableItems = modelFacade.SelectAllAsDictionary(cmsClass.ClassTableName); + foreach (var customTableItem in customTableItems) + { + var results = mapper.Map(new CustomTableMapperSource( + targetDataClass.ClassFormDefinition, + cmsClass.ClassFormDefinition, + customTableItem.TryGetValue("ItemGUID", out object? itemGuid) && itemGuid is Guid guid ? guid : Guid.NewGuid(), // TODO tomas.krch: 2024-12-03 provide guid? + cmsClass, + customTableItem, + mapping + )); + try + { + var commonDataInfos = new List(); + foreach (var umtModel in results) + { + switch (await importer.ImportAsync(umtModel)) + { + case { Success: false } result: + { + logger.LogError("Failed to import: {Exception}, {ValidationResults}", result.Exception, JsonConvert.SerializeObject(result.ModelValidationResults)); + break; + } + case { Success: true, Imported: ContentItemCommonDataInfo ccid }: + { + commonDataInfos.Add(ccid); + Debug.Assert(ccid.ContentItemCommonDataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); + break; + } + case { Success: true, Imported: ContentItemLanguageMetadataInfo cclm }: + { + Debug.Assert(cclm.ContentItemLanguageMetadataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); + break; + } + default: + break; + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error occured while mapping custom table item to content item"); + } + } + } } private async Task SaveClassUsingKxoApi(ICmsClass srcClass, DataClassInfo kxoDataClass) diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs index b1502414..3cd3fe9c 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs @@ -1,6 +1,5 @@ using CMS.ContentEngine; using CMS.DataEngine; -using CMS.FormEngine; using MediatR; using Microsoft.Data.SqlClient; @@ -8,15 +7,13 @@ using Migration.Tool.Common; using Migration.Tool.Common.Abstractions; -using Migration.Tool.Common.Builders; using Migration.Tool.Common.Helpers; using Migration.Tool.Common.MigrationProtocol; using Migration.Tool.KXP.Api; -using Migration.Tool.KXP.Api.Services.CmsClass; using Migration.Tool.Source.Contexts; using Migration.Tool.Source.Helpers; -using Migration.Tool.Source.Mappers; using Migration.Tool.Source.Model; +using Migration.Tool.Source.Providers; using Migration.Tool.Source.Services; namespace Migration.Tool.Source.Handlers; @@ -31,9 +28,7 @@ public class MigratePageTypesCommandHandler( ModelFacade modelFacade, PageTemplateMigrator pageTemplateMigrator, ReusableSchemaService reusableSchemaService, - IEnumerable classMappings, - IFieldMigrationService fieldMigrationService, - IEnumerable reusableSchemaBuilders + ClassMappingProvider classMappingProvider ) : IRequestHandler { @@ -48,138 +43,7 @@ public async Task Handle(MigratePageTypesCommand request, Cancell .OrderBy(x => x.ClassID) ); - ExecReusableSchemaBuilders(); - - var manualMappings = new Dictionary(); - foreach (var classMapping in classMappings) - { - var newDt = DataClassInfoProvider.GetDataClassInfo(classMapping.TargetClassName) ?? DataClassInfo.New(); - classMapping.PatchTargetDataClass(newDt); - - // might not need ClassGUID - // newDt.ClassGUID = GuidHelper.CreateDataClassGuid($"{newDt.ClassName}|{newDt.ClassTableName}"); - - var cmsClasses = new List(); - foreach (string sourceClassName in classMapping.SourceClassNames) - { - cmsClasses.AddRange(modelFacade.SelectWhere("ClassName=@className", new SqlParameter("className", sourceClassName))); - } - - var nfi = string.IsNullOrWhiteSpace(newDt.ClassFormDefinition) ? new FormInfo() : new FormInfo(newDt.ClassFormDefinition); - bool hasPrimaryKey = false; - foreach (var formFieldInfo in nfi.GetFields(true, true, true, true, false)) - { - if (formFieldInfo.PrimaryKey) - { - hasPrimaryKey = true; - } - } - - if (!hasPrimaryKey) - { - if (string.IsNullOrWhiteSpace(classMapping.PrimaryKey)) - { - throw new InvalidOperationException($"Class mapping has no primary key set"); - } - else - { - var prototype = FormHelper.GetBasicFormDefinition(classMapping.PrimaryKey); - nfi.AddFormItem(prototype.GetFormField(classMapping.PrimaryKey)); - } - } - - newDt.ClassFormDefinition = nfi.GetXmlDefinition(); - - foreach (string schemaName in classMapping.ReusableSchemaNames) - { - reusableSchemaService.AddReusableSchemaToDataClass(newDt, schemaName); - } - - nfi = new FormInfo(newDt.ClassFormDefinition); - - var fieldInReusableSchemas = reusableSchemaService.GetFieldsFromReusableSchema(newDt).ToDictionary(x => x.Name, x => x); - - bool hasFieldsAlready = true; - foreach (var cmml in classMapping.Mappings.Where(m => m.IsTemplate).ToLookup(x => x.SourceFieldName)) - { - foreach (var cmm in cmml) - { - if (fieldInReusableSchemas.ContainsKey(cmm.TargetFieldName)) - { - // part of reusable schema - continue; - } - - var sc = cmsClasses.FirstOrDefault(sc => sc.ClassName.Equals(cmm.SourceClassName, StringComparison.InvariantCultureIgnoreCase)) - ?? throw new NullReferenceException($"The source class '{cmm.SourceClassName}' does not exist - wrong mapping {classMapping}"); - - var fi = new FormInfo(sc.ClassFormDefinition); - if (nfi.GetFormField(cmm.TargetFieldName) is { }) - { - } - else - { - var src = fi.GetFormField(cmm.SourceFieldName); - src.Name = cmm.TargetFieldName; - nfi.AddFormItem(src); - hasFieldsAlready = false; - } - } - //var cmm = cmml.FirstOrDefault() ?? throw new InvalidOperationException(); - } - - if (!hasFieldsAlready) - { - FormDefinitionHelper.MapFormDefinitionFields(logger, fieldMigrationService, nfi.GetXmlDefinition(), false, true, newDt, false, false); - CmsClassMapper.PatchDataClassInfo(newDt, out _, out _); - } - - if (classMapping.TargetFieldPatchers.Count > 0) - { - nfi = new FormInfo(newDt.ClassFormDefinition); - foreach (string fieldName in classMapping.TargetFieldPatchers.Keys) - { - classMapping.TargetFieldPatchers[fieldName].Invoke(nfi.GetFormField(fieldName)); - } - - newDt.ClassFormDefinition = nfi.GetXmlDefinition(); - } - - DataClassInfoProvider.SetDataClassInfo(newDt); - foreach (var gByClass in classMapping.Mappings.GroupBy(x => x.SourceClassName)) - { - manualMappings.TryAdd(gByClass.Key, newDt); - } - - foreach (string sourceClassName in classMapping.SourceClassNames) - { - if (newDt.ClassContentTypeType is ClassContentTypeType.REUSABLE) - { - continue; - } - - var sourceClass = cmsClasses.First(c => c.ClassName.Equals(sourceClassName, StringComparison.InvariantCultureIgnoreCase)); - foreach (var cmsClassSite in modelFacade.SelectWhere("ClassId = @classId", new SqlParameter("classId", sourceClass.ClassID))) - { - if (modelFacade.SelectById(cmsClassSite.SiteID) is { SiteGUID: var siteGuid }) - { - if (ChannelInfoProvider.ProviderObject.Get(siteGuid) is { ChannelID: var channelId }) - { - var info = new ContentTypeChannelInfo { ContentTypeChannelChannelID = channelId, ContentTypeChannelContentTypeID = newDt.ClassID }; - ContentTypeChannelInfoProvider.ProviderObject.Set(info); - } - else - { - logger.LogWarning("Channel for site with SiteGUID '{SiteGuid}' not found", siteGuid); - } - } - else - { - logger.LogWarning("Source site with SiteID '{SiteId}' not found", cmsClassSite.SiteID); - } - } - } - } + var manualMappings = classMappingProvider.ExecuteMappings(); while (ksClasses.GetNext(out var di)) { @@ -276,61 +140,6 @@ public async Task Handle(MigratePageTypesCommand request, Cancell return new GenericCommandResult(); } - private void ExecReusableSchemaBuilders() - { - foreach (var reusableSchemaBuilder in reusableSchemaBuilders) - { - reusableSchemaBuilder.AssertIsValid(); - var fieldInfos = reusableSchemaBuilder.FieldBuilders.Select(fb => - { - switch (fb) - { - case { Factory: { } factory }: - { - return factory(); - } - case { SourceFieldIdentifier: { } fieldIdentifier }: - { - var sourceClass = modelFacade.SelectWhere("ClassName=@className", new SqlParameter("className", fieldIdentifier.ClassName)).SingleOrDefault() - ?? throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fieldIdentifier.ClassName}': DataClass not found, class name '{fieldIdentifier.ClassName}'"); - - if (string.IsNullOrWhiteSpace(sourceClass.ClassFormDefinition)) - { - throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fieldIdentifier.ClassName}': Class '{fieldIdentifier.ClassName}' is missing field '{fieldIdentifier.FieldName}'"); - } - - // this might be cached as optimization - var patcher = new FormDefinitionPatcher( - logger, - sourceClass.ClassFormDefinition, - fieldMigrationService, - sourceClass.ClassIsForm.GetValueOrDefault(false), - sourceClass.ClassIsDocumentType, - true, - false - ); - - patcher.PatchFields(); - patcher.RemoveCategories(); - - var fi = new FormInfo(patcher.GetPatched()); - return fi.GetFormField(fieldIdentifier.FieldName) switch - { - { } field => field, - _ => throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fieldIdentifier.ClassName}': Class '{fieldIdentifier.ClassName}' is missing field '{fieldIdentifier.FieldName}'") - }; - } - default: - { - throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fb.TargetFieldName}'"); - } - } - }); - - reusableSchemaService.EnsureReusableFieldSchema(reusableSchemaBuilder.SchemaName, reusableSchemaBuilder.SchemaDisplayName, reusableSchemaBuilder.SchemaDescription, fieldInfos.ToArray()); - } - } - private async Task MigratePageTemplateConfigurations() { if (modelFacade.IsAvailable()) diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs index 0ca85607..0bdc8347 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs @@ -46,8 +46,6 @@ ClassMappingProvider classMappingProvider { private const string ClassCmsRoot = "CMS.Root"; - private readonly ContentItemNameProvider contentItemNameProvider = new(new ContentItemNameValidator()); - private readonly ConcurrentDictionary languages = new(StringComparer.InvariantCultureIgnoreCase); public async Task Handle(MigratePagesCommand request, CancellationToken cancellationToken) @@ -183,7 +181,7 @@ public async Task Handle(MigratePagesCommand request, Cancellatio ); } - string safeNodeName = await contentItemNameProvider.Get(ksNode.NodeName); + string safeNodeName = await Service.Resolve().Get(ksNode.NodeName); var ksNodeParent = modelFacade.SelectById(ksNode.NodeParentID); var nodeParentGuid = ksNodeParent?.NodeAliasPath == "/" || ksNodeParent == null ? (Guid?)null diff --git a/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs b/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs index 4df44c23..92494567 100644 --- a/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs +++ b/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs @@ -74,7 +74,7 @@ public static IServiceCollection UseKsToolCore(this IServiceCollection services, services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(KsCoreDiExtensions).Assembly)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestHandlingBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CommandConstraintBehavior<,>)); - services.AddTransient(typeof(IPipelineBehavior<,>), typeof(XbKApiContextBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(XbyKApiContextBehavior<,>)); services.AddSingleton(); services.AddSingleton(); @@ -87,6 +87,7 @@ public static IServiceCollection UseKsToolCore(this IServiceCollection services, // umt mappers services.AddTransient, ContentItemMapper>(); + services.AddTransient, ContentItemMapper>(); services.AddTransient, TagMapper>(); // mappers diff --git a/KVA/Migration.Tool.Source/Mappers/CmsClassMapper.cs b/KVA/Migration.Tool.Source/Mappers/CmsClassMapper.cs index 33d9ac95..87ca1499 100644 --- a/KVA/Migration.Tool.Source/Mappers/CmsClassMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/CmsClassMapper.cs @@ -268,7 +268,7 @@ public static DataClassInfo PatchDataClassInfo(DataClassInfo dataClass, out stri public static string? GetLegacyDocumentName(FormInfo nfi, string className) { - if (nfi.GetFields(true, true).FirstOrDefault(f => GuidHelper.CreateDocumentNameFieldGuid($"documentname|{className}").Equals(f.Guid)) is { } foundField) + if (nfi.GetFields(true, true).FirstOrDefault(f => GuidHelper.CreateFieldGuid($"documentname|{className}").Equals(f.Guid)) is { } foundField) { return foundField.Name; } @@ -276,9 +276,9 @@ public static DataClassInfo PatchDataClassInfo(DataClassInfo dataClass, out stri return null; } - private static void AppendDocumentNameField(FormInfo nfi, string className, out string documentNameField) + private static void AppendDocumentNameField(FormInfo nfi, string newClassName, out string documentNameField) { - if (GetLegacyDocumentName(nfi, className) is { } fieldName) + if (GetLegacyDocumentName(nfi, newClassName) is { } fieldName) { documentNameField = fieldName; return; @@ -301,7 +301,7 @@ private static void AppendDocumentNameField(FormInfo nfi, string className, out Size = 100, Precision = 0, DefaultValue = null, - Guid = GuidHelper.CreateDocumentNameFieldGuid($"documentname|{className}"), + Guid = GuidHelper.CreateFieldGuid($"documentname|{newClassName}"), System = false, // no longer system field, system doesn't rely on this field anymore Settings = { { "controlname", "Kentico.Administration.TextInput" } } }); diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs index 231259b6..fe6813c2 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs @@ -8,6 +8,7 @@ using CMS.Websites; using CMS.Websites.Internal; using Kentico.Xperience.UMT.Model; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Migration.Tool.Common; using Migration.Tool.Common.Abstractions; @@ -38,6 +39,15 @@ public record CmsTreeMapperSource( ICmsSite SourceSite ); +public record CustomTableMapperSource( + string? TargetFormDefinition, + string SourceFormDefinition, + Guid ContentItemGuid, + ICmsClass SourceClass, + Dictionary Values, + IClassMapping ClassMapping +); + public class ContentItemMapper( ILogger logger, CoupledDataService coupledDataService, @@ -52,8 +62,9 @@ public class ContentItemMapper( MediaLinkServiceFactory mediaLinkServiceFactory, ToolConfiguration configuration, ClassMappingProvider classMappingProvider, - PageBuilderPatcher pageBuilderPatcher - ) : UmtMapperBase + PageBuilderPatcher pageBuilderPatcher, + IServiceProvider serviceProvider + ) : UmtMapperBase, IUmtMapper { private const string CLASS_FIELD_CONTROL_NAME = "controlname"; @@ -66,7 +77,7 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source var sourceNodeClass = modelFacade.SelectById(cmsTree.NodeClassID) ?? throw new InvalidOperationException($"Fatal: node class is missing, class id '{cmsTree.NodeClassID}'"); var mapping = classMappingProvider.GetMapping(sourceNodeClass.ClassName); var targetClassGuid = sourceNodeClass.ClassGUID; - DataClassInfo targetClassInfo = null; + var targetClassInfo = DataClassInfoProvider.ProviderObject.Get(sourceNodeClass.ClassName); if (mapping != null) { targetClassInfo = DataClassInfoProvider.ProviderObject.Get(mapping.TargetClassName) ?? throw new InvalidOperationException($"Unable to find target class '{mapping.TargetClassName}'"); @@ -77,6 +88,11 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source var contentItemGuid = spoiledGuidContext.EnsureNodeGuid(cmsTree.NodeGUID, cmsTree.NodeSiteID, cmsTree.NodeID); bool isMappedTypeReusable = (targetClassInfo?.ClassContentTypeType is ClassContentTypeType.REUSABLE) || configuration.ClassNamesConvertToContentHub.Contains(sourceNodeClass.ClassName); + if (isMappedTypeReusable) + { + logger.LogTrace("Target is reusable {Info}", new { cmsTree.NodeAliasPath, targetClassInfo?.ClassName }); + } + yield return new ContentItemModel { ContentItemGUID = contentItemGuid, @@ -269,17 +285,24 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source var targetColumns = commonFields .Select(cf => ReusableSchemaService.RemoveClassPrefix(sourceNodeClass.ClassName, cf.Name)) .Union(fi.GetColumnNames(false)) - .Except([CmsClassMapper.GetLegacyDocumentName(fi, sourceNodeClass.ClassName)]) + .Except([CmsClassMapper.GetLegacyDocumentName(fi, targetClassInfo?.ClassName)]) .ToList(); var coupledDataRow = coupledDataService.GetSourceCoupledDataRow(sourceNodeClass.ClassTableName, primaryKeyName, cmsDocument.DocumentForeignKeyValue); + var sourceObjectContext = new DocumentSourceObjectContext(cmsTree, sourceNodeClass, sourceSite, sfi, fi, cmsDocument.DocumentID); + var convertorContext = new ConvertorTreeNodeContext(cmsTree.NodeGUID, cmsTree.NodeSiteID, cmsDocument.DocumentID, false); // TODO tomas.krch: 2024-09-05 propagate async to root MapCoupledDataFieldValues(dataModel.CustomProperties, columnName => coupledDataRow?[columnName], columnName => coupledDataRow?.ContainsKey(columnName) ?? false, - cmsTree, cmsDocument.DocumentID, + // cmsTree, cmsDocument.DocumentID, targetColumns, sfi, fi, - false, sourceNodeClass, sourceSite, mapping + false, sourceNodeClass, + // sourceSite, + mapping, + sourceObjectContext, + convertorContext, + targetClassInfo.ClassName ).GetAwaiter().GetResult(); foreach (var formFieldInfo in commonFields) @@ -300,7 +323,7 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source } string targetClassName = mapping?.TargetClassName ?? sourceNodeClass.ClassName; - if (CmsClassMapper.GetLegacyDocumentName(fi, targetClassName) is { } legacyDocumentNameFieldName) + if (CmsClassMapper.GetLegacyDocumentName(fi, targetClassInfo.ClassName) is { } legacyDocumentNameFieldName) { if (reusableSchemaService.IsConversionToReusableFieldSchemaRequested(targetClassName)) { @@ -375,6 +398,9 @@ private IEnumerable MigrateDraft(ICmsVersionHistory checkoutVersion, ContentItemCommonDataModel? commonDataModel = null; ContentItemDataModel? dataModel = null; + + string targetClassName = mapping?.TargetClassName ?? sourceNodeClass.ClassName; + try { string? pageTemplateConfiguration = adapter.DocumentPageTemplateConfiguration; @@ -452,14 +478,23 @@ private IEnumerable MigrateDraft(ICmsVersionHistory checkoutVersion, var sourceColumns = commonFields .Select(cf => ReusableSchemaService.RemoveClassPrefix(sourceNodeClass.ClassName, cf.Name)) .Union(fi.GetColumnNames(false)) - .Except([CmsClassMapper.GetLegacyDocumentName(fi, sourceNodeClass.ClassName)]) + .Except([CmsClassMapper.GetLegacyDocumentName(fi, targetClassName)]) .ToList(); + var sourceObjectContext = new DocumentSourceObjectContext(cmsTree, sourceNodeClass, sourceSite, sfi, fi, adapter.DocumentID); + var convertorContext = new ConvertorTreeNodeContext(cmsTree.NodeGUID, cmsTree.NodeSiteID, adapter.DocumentID, false); // TODO tomas.krch: 2024-09-05 propagate async to root MapCoupledDataFieldValues(dataModel.CustomProperties, s => adapter.GetValue(s), - s => adapter.HasValueSet(s) - , cmsTree, adapter.DocumentID, sourceColumns, sfi, fi, true, sourceNodeClass, sourceSite, mapping).GetAwaiter().GetResult(); + s => adapter.HasValueSet(s), + // cmsTree, adapter.DocumentID, + sourceColumns, sfi, fi, true, sourceNodeClass, + // sourceSite, + mapping, + sourceObjectContext, + convertorContext, + targetClassName + ).GetAwaiter().GetResult(); foreach (var formFieldInfo in commonFields) { @@ -503,26 +538,26 @@ private IEnumerable MigrateDraft(ICmsVersionHistory checkoutVersion, } private async Task MapCoupledDataFieldValues( - Dictionary target, - Func getSourceValue, - Func containsSourceValue, - ICmsTree cmsTree, - int? documentId, - List newColumnNames, - FormInfo oldFormInfo, - FormInfo newFormInfo, - bool migratingFromVersionHistory, - ICmsClass sourceNodeClass, - ICmsSite site, - IClassMapping mapping - ) + Dictionary target, + Func getSourceValue, + Func containsSourceValue, + List newColumnNames, + FormInfo oldFormInfo, + FormInfo newFormInfo, + bool migratingFromVersionHistory, + ICmsClass sourceNodeClass, + IClassMapping mapping, + ISourceObjectContext sourceObjectContext, + IConvertorContext convertorContext, + string targetClassName + ) { Debug.Assert(sourceNodeClass.ClassTableName != null, "sourceNodeClass.ClassTableName != null"); foreach (string targetColumnName in newColumnNames) { string targetFieldName = null!; - Func valueConvertor = sourceValue => sourceValue; + Func valueConvertor = (sourceValue, _) => sourceValue; switch (mapping?.GetMapping(targetColumnName, sourceNodeClass.ClassName)) { case FieldMappingWithConversion fieldMappingWithConversion: @@ -534,13 +569,13 @@ IClassMapping mapping case FieldMapping fieldMapping: { targetFieldName = fieldMapping.TargetFieldName; - valueConvertor = sourceValue => sourceValue; + valueConvertor = (sourceValue, _) => sourceValue; break; } case null: { targetFieldName = targetColumnName; - valueConvertor = sourceValue => sourceValue; + valueConvertor = (sourceValue, _) => sourceValue; break; } @@ -552,7 +587,7 @@ IClassMapping mapping targetFieldName.Equals("ContentItemDataID", StringComparison.InvariantCultureIgnoreCase) || targetFieldName.Equals("ContentItemDataCommonDataID", StringComparison.InvariantCultureIgnoreCase) || targetFieldName.Equals("ContentItemDataGUID", StringComparison.InvariantCultureIgnoreCase) || - targetFieldName.Equals(CmsClassMapper.GetLegacyDocumentName(newFormInfo, sourceNodeClass.ClassName), StringComparison.InvariantCultureIgnoreCase) + targetFieldName.Equals(CmsClassMapper.GetLegacyDocumentName(newFormInfo, targetClassName), StringComparison.InvariantCultureIgnoreCase) ) { logger.LogTrace("Skipping '{FieldName}'", targetFieldName); @@ -587,28 +622,29 @@ IClassMapping mapping string? controlName = field.Settings[CLASS_FIELD_CONTROL_NAME]?.ToString()?.ToLowerInvariant(); object? sourceValue = getSourceValue(sourceFieldName); - target[targetFieldName] = valueConvertor.Invoke(sourceValue); - var fvmc = new FieldMigrationContext(field.DataType, controlName, targetColumnName, new DocumentSourceObjectContext(cmsTree, sourceNodeClass, site, oldFormInfo, newFormInfo, documentId)); + target[targetFieldName] = valueConvertor.Invoke(sourceValue, convertorContext); + var fvmc = new FieldMigrationContext(field.DataType, controlName, targetColumnName, sourceObjectContext); var fmb = fieldMigrationService.GetFieldMigration(fvmc); if (fmb is FieldMigration fieldMigration) { + var documentSourceObjectContext = sourceObjectContext as DocumentSourceObjectContext; if (controlName != null) { - if (fieldMigration.Actions?.Contains(TcaDirective.ConvertToPages) ?? false) + if ((fieldMigration.Actions?.Contains(TcaDirective.ConvertToPages) ?? false) && documentSourceObjectContext != null) { // relation to other document - var convertedRelation = relationshipService.GetNodeRelationships(cmsTree.NodeID, sourceNodeClass.ClassName, field.Guid) + var convertedRelation = relationshipService.GetNodeRelationships(documentSourceObjectContext.CmsTree.NodeID, sourceNodeClass.ClassName, field.Guid) .Select(r => new WebPageRelatedItem { WebPageGuid = spoiledGuidContext.EnsureNodeGuid(r.RightNode.NodeGUID, r.RightNode.NodeSiteID, r.RightNode.NodeID) }); - target.SetValueAsJson(targetFieldName, valueConvertor.Invoke(convertedRelation)); + target.SetValueAsJson(targetFieldName, valueConvertor.Invoke(convertedRelation, convertorContext)); } else { // leave as is - target[targetFieldName] = valueConvertor.Invoke(sourceValue); + target[targetFieldName] = valueConvertor.Invoke(sourceValue, convertorContext); } - if (fieldMigration.TargetFormComponent == "webpages") + if (fieldMigration.TargetFormComponent == "webpages" && documentSourceObjectContext != null) { if (sourceValue is string pageReferenceJson) { @@ -617,18 +653,18 @@ IClassMapping mapping { if (jToken.Path.EndsWith("NodeGUID", StringComparison.InvariantCultureIgnoreCase)) { - var patchedGuid = spoiledGuidContext.EnsureNodeGuid(jToken.Value(), cmsTree.NodeSiteID); + var patchedGuid = spoiledGuidContext.EnsureNodeGuid(jToken.Value(), documentSourceObjectContext.CmsTree.NodeSiteID); jToken.Replace(JToken.FromObject(patchedGuid)); } } - target[targetFieldName] = valueConvertor.Invoke(parsed.ToString().Replace("\"NodeGuid\"", "\"WebPageGuid\"")); + target[targetFieldName] = valueConvertor.Invoke(parsed.ToString().Replace("\"NodeGuid\"", "\"WebPageGuid\""), convertorContext); } } } else { - target[targetFieldName] = valueConvertor.Invoke(sourceValue); + target[targetFieldName] = valueConvertor.Invoke(sourceValue, convertorContext); } } else if (fmb != null) @@ -637,7 +673,7 @@ IClassMapping mapping { case { Success: true } result: { - target[targetFieldName] = valueConvertor.Invoke(result.MigratedValue); + target[targetFieldName] = valueConvertor.Invoke(result.MigratedValue, convertorContext); break; } case { Success: false }: @@ -652,7 +688,7 @@ IClassMapping mapping } else { - target[targetFieldName] = valueConvertor?.Invoke(sourceValue); + target[targetFieldName] = valueConvertor?.Invoke(sourceValue, convertorContext); } @@ -670,46 +706,48 @@ IClassMapping mapping { var mediaLinkService = mediaLinkServiceFactory.Create(); var htmlProcessor = new HtmlProcessor(html, mediaLinkService); - - target[targetColumnName] = await htmlProcessor.ProcessHtml(site.SiteID, async (result, original) => + if (sourceObjectContext is DocumentSourceObjectContext documentSourceObjectContext) { - switch (result) + target[targetColumnName] = await htmlProcessor.ProcessHtml(documentSourceObjectContext.Site.SiteID, async (result, original) => { - case { LinkKind: MediaLinkKind.Guid or MediaLinkKind.DirectMediaPath, MediaKind: MediaKind.MediaFile }: + switch (result) { - var mediaFile = MediaHelper.GetMediaFile(result, modelFacade); - if (mediaFile is null) + case { LinkKind: MediaLinkKind.Guid or MediaLinkKind.DirectMediaPath, MediaKind: MediaKind.MediaFile }: { - return original; - } + var mediaFile = MediaHelper.GetMediaFile(result, modelFacade); + if (mediaFile is null) + { + return original; + } - return assetFacade.GetAssetUri(mediaFile); - } - case { LinkKind: MediaLinkKind.Guid, MediaKind: MediaKind.Attachment, MediaGuid: { } mediaGuid, LinkSiteId: var linkSiteId }: - { - var attachment = MediaHelper.GetAttachment(result, modelFacade); - if (attachment is null) - { - return original; + return assetFacade.GetAssetUri(mediaFile); } + case { LinkKind: MediaLinkKind.Guid, MediaKind: MediaKind.Attachment, MediaGuid: { } mediaGuid, LinkSiteId: var linkSiteId }: + { + var attachment = MediaHelper.GetAttachment(result, modelFacade); + if (attachment is null) + { + return original; + } - await attachmentMigrator.MigrateAttachment(attachment); + await attachmentMigrator.MigrateAttachment(attachment); - string? culture = null; - if (attachment.AttachmentDocumentID is { } attachmentDocumentId) - { - culture = modelFacade.SelectById(attachmentDocumentId)?.DocumentCulture; + string? culture = null; + if (attachment.AttachmentDocumentID is { } attachmentDocumentId) + { + culture = modelFacade.SelectById(attachmentDocumentId)?.DocumentCulture; + } + + return assetFacade.GetAssetUri(attachment, culture); } - return assetFacade.GetAssetUri(attachment, culture); + default: + break; } - default: - break; - } - - return original; - }); + return original; + }); + } } } } @@ -737,4 +775,171 @@ private static IEnumerable UnpackReusableFieldSchemas(IEnumerable } + public IEnumerable Map(CustomTableMapperSource source) + { + // only reusable items + (string? targetFormDefinition, string sourceFormDefinition, var contentItemGuid, var sourceClass, var values, var classMapping) = source; + + var mapping = classMappingProvider.GetMapping(sourceClass.ClassName); + var targetClassGuid = sourceClass.ClassGUID; + var targetClassInfo = DataClassInfoProvider.ProviderObject.Get(sourceClass.ClassName); + if (mapping != null) + { + targetClassInfo = DataClassInfoProvider.ProviderObject.Get(mapping.TargetClassName) ?? throw new InvalidOperationException($"Unable to find target class '{mapping.TargetClassName}'"); + targetClassGuid = targetClassInfo.ClassGUID; + } + + var mappingHandler = typeof(DefaultCustomTableClassMappingHandler); + if (classMapping.MappingHandler is not null) + { + mappingHandler = classMapping.MappingHandler; + } + + if (serviceProvider.GetRequiredService(mappingHandler) is not IClassMappingHandler mappingHandlerInstance) + { + throw new InvalidOperationException($"Incorrect handler registered '{mappingHandler.FullName}'"); + } + + IClassMappingHandler handler = new ClassMappingHandlerWrapper(mappingHandlerInstance, logger); + var ctms = new CustomTableMappingHandlerContext(values, targetClassInfo, sourceClass.ClassName); + + bool isMappedTypeReusable = targetClassInfo?.ClassContentTypeType is ClassContentTypeType.REUSABLE; // TODO tomas.krch: 2024-12-03 configuration here? || configuration.ClassNamesConvertToContentHub.Contains(sourceNodeClass.ClassName); + if (!isMappedTypeReusable) + { + throw new InvalidOperationException("Mapping of custom table items to web site channel is currently not supported"); + } + + var contentItemModel = new ContentItemModel { ContentItemGUID = contentItemGuid, ContentItemIsReusable = isMappedTypeReusable, ContentItemDataClassGuid = targetClassGuid, }; + + handler.EnsureContentItem(contentItemModel, ctms); + + yield return contentItemModel; + + var versionStatus = VersionStatus.Published; + + + DateTime? scheduledPublishWhen = null; + DateTime? scheduleUnpublishWhen = null; + string? contentItemCommonDataPageBuilderWidgets = null; + string? contentItemCommonDataPageTemplateConfiguration = null; + + + // todo async + var languageVersions = handler.ProduceLanguageVersions(ctms).GetAwaiter().GetResult(); + foreach (var (contentLanguageInfo, languageSensitiveValues) in languageVersions) + { + var commonDataModel = new ContentItemCommonDataModel + { + ContentItemCommonDataContentItemGuid = contentItemGuid, + ContentItemCommonDataContentLanguageGuid = contentLanguageInfo.ContentLanguageGUID, + ContentItemCommonDataVersionStatus = versionStatus, + ContentItemCommonDataPageBuilderWidgets = contentItemCommonDataPageBuilderWidgets, + ContentItemCommonDataPageTemplateConfiguration = contentItemCommonDataPageTemplateConfiguration, + }; + + handler.EnsureContentItemCommonData(commonDataModel, ctms); + + var dataModel = new ContentItemDataModel + { + ContentItemDataGUID = commonDataModel.ContentItemCommonDataGUID, + ContentItemDataCommonDataGuid = commonDataModel.ContentItemCommonDataGUID, + ContentItemContentTypeName = mapping?.TargetClassName ?? targetClassInfo?.ClassName + }; + + var fi = new FormInfo(targetFormDefinition); + var sfi = new FormInfo(sourceFormDefinition); + string primaryKeyName = ""; + foreach (var sourceFieldInfo in sfi.GetFields(true, true)) + { + if (sourceFieldInfo.PrimaryKey) + { + primaryKeyName = sourceFieldInfo.Name; + } + } + + if (string.IsNullOrWhiteSpace(primaryKeyName)) + { + throw new Exception("Error, unable to find coupled data primary key"); + } + + var commonFields = UnpackReusableFieldSchemas(fi.GetFields()).ToArray(); + var targetColumns = commonFields + .Select(cf => ReusableSchemaService.RemoveClassPrefix(sourceClass.ClassName, cf.Name)) + .Union(fi.GetColumnNames(false)) + .Except([CmsClassMapper.GetLegacyDocumentName(fi, targetClassInfo.ClassName)]) + .ToList(); + + var sourceObjectContext = new CustomTableSourceObjectContext(); + var convertorContext = new ConvertorCustomTableContext(); + + // TODO tomas.krch: 2024-09-05 propagate async to root + MapCoupledDataFieldValues(dataModel.CustomProperties, + columnName => languageSensitiveValues[columnName], + columnName => languageSensitiveValues.ContainsKey(columnName), + // cmsTree, cmsDocument.DocumentID, + targetColumns, sfi, fi, + false, sourceClass, + //sourceSite, + mapping, + sourceObjectContext, + convertorContext, + targetClassInfo.ClassName + ).GetAwaiter().GetResult(); + + string? documentNameFieldName = CmsClassMapper.GetLegacyDocumentName(fi, targetClassInfo.ClassName); + if (documentNameFieldName is not null) + { + dataModel.CustomProperties[documentNameFieldName] = contentItemModel.ContentItemName; + } + + foreach (var formFieldInfo in commonFields) + { + string originalFieldName = ReusableSchemaService.RemoveClassPrefix(sourceClass.ClassName, formFieldInfo.Name); + if (dataModel.CustomProperties.TryGetValue(originalFieldName, out object? value)) + { + commonDataModel.CustomProperties ??= []; + logger.LogTrace("Reusable schema field '{FieldName}' from schema '{SchemaGuid}' populated", formFieldInfo.Name, formFieldInfo.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY]); + commonDataModel.CustomProperties[formFieldInfo.Name] = value; + dataModel.CustomProperties.Remove(originalFieldName); + } + else + { + logger.LogTrace("Reusable schema field '{FieldName}' from schema '{SchemaGuid}' missing", formFieldInfo.Name, formFieldInfo.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY]); + } + } + + yield return commonDataModel; + yield return dataModel; + + Guid? documentCreatedByUserGuid = null; + int? createdByUserId = handler.GetCreatedByUserId(ctms, sourceClass.ClassName, sourceClass.ClassFormDefinition); + if (createdByUserId.HasValue && modelFacade.TrySelectGuid(createdByUserId, out var createdByUserGuid)) + { + documentCreatedByUserGuid = createdByUserGuid; + } + + Guid? documentModifiedByUserGuid = null; + int? modifiedByUserId = handler.GetModifiedByUserId(ctms, sourceClass.ClassName, sourceClass.ClassFormDefinition); + if (modelFacade.TrySelectGuid(modifiedByUserId, out var modifiedByUserGuid)) + { + documentModifiedByUserGuid = modifiedByUserGuid; + } + + var languageMetadataInfo = new ContentItemLanguageMetadataModel + { + ContentItemLanguageMetadataContentItemGuid = contentItemGuid, + ContentItemLanguageMetadataLatestVersionStatus = VersionStatus.Published, // That's the latest status of th item for admin optimization + ContentItemLanguageMetadataCreatedByUserGuid = documentCreatedByUserGuid, + ContentItemLanguageMetadataModifiedByUserGuid = documentModifiedByUserGuid, + ContentItemLanguageMetadataHasImageAsset = false, + ContentItemLanguageMetadataContentLanguageGuid = commonDataModel.ContentItemCommonDataContentLanguageGuid, // DocumentCulture -> language entity needs to be created and its ID used here + ContentItemLanguageMetadataScheduledPublishWhen = scheduledPublishWhen, + ContentItemLanguageMetadataScheduledUnpublishWhen = scheduleUnpublishWhen + }; + + handler.EnsureContentItemLanguageMetadata(languageMetadataInfo, ctms); + + yield return languageMetadataInfo; + } + } } diff --git a/KVA/Migration.Tool.Source/Mappers/ResourceMapper.cs b/KVA/Migration.Tool.Source/Mappers/ResourceMapper.cs index e3e8ff1f..b28f954e 100644 --- a/KVA/Migration.Tool.Source/Mappers/ResourceMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/ResourceMapper.cs @@ -27,11 +27,11 @@ protected override ResourceInfo MapInternal(ICmsResource source, ResourceInfo ta if (target.ResourceName == Kx13SystemResource.Licenses) { - target.ResourceName = XbkSystemResource.CMS_Licenses; - logger.LogInformation("Patching CMS Resource 'Licences': name changed to '{ResourceNamePatched}'", XbkSystemResource.CMS_Licenses); + target.ResourceName = XbyKSystemResource.CMS_Licenses; + logger.LogInformation("Patching CMS Resource 'Licences': name changed to '{ResourceNamePatched}'", XbyKSystemResource.CMS_Licenses); } - if (!XbkSystemResource.All.Contains(target.ResourceName) || Kx13SystemResource.ConvertToNonSysResource.Contains(target.ResourceName)) + if (!XbyKSystemResource.All.Contains(target.ResourceName) || Kx13SystemResource.ConvertToNonSysResource.Contains(target.ResourceName)) { // custom resource diff --git a/KVA/Migration.Tool.Source/Migration.Tool.Source.csproj b/KVA/Migration.Tool.Source/Migration.Tool.Source.csproj index 220d510b..9344a5c2 100644 --- a/KVA/Migration.Tool.Source/Migration.Tool.Source.csproj +++ b/KVA/Migration.Tool.Source/Migration.Tool.Source.csproj @@ -12,7 +12,7 @@ - + diff --git a/KVA/Migration.Tool.Source/ModelFacade.cs b/KVA/Migration.Tool.Source/ModelFacade.cs index 13f26292..87dd3df1 100644 --- a/KVA/Migration.Tool.Source/ModelFacade.cs +++ b/KVA/Migration.Tool.Source/ModelFacade.cs @@ -1,12 +1,12 @@ using System.Runtime.CompilerServices; using Microsoft.Data.SqlClient; - +using Microsoft.Extensions.Logging; using Migration.Tool.Common; namespace Migration.Tool.Source; -public class ModelFacade(ToolConfiguration configuration) +public class ModelFacade(ILogger logger, ToolConfiguration configuration) { private SemanticVersion? semanticVersion; @@ -44,6 +44,47 @@ public IEnumerable SelectAll(string? orderBy = null) where T : ISourceMode } } + public IEnumerable> SelectAllAsDictionary(string tableName, string? orderBy = null) + { + semanticVersion ??= SelectVersion(); + using var conn = GetConnection(); + conn.Open(); + var cmd = conn.CreateCommand(); + cmd.CommandText = $"SELECT * FROM {tableName}"; + if (!string.IsNullOrWhiteSpace(orderBy)) + { + cmd.CommandText += orderBy; + } + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var r = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (var dbColumn in reader.GetColumnSchema()) + { + if (dbColumn.ColumnOrdinal is { } ordinal) + { + object val = reader.GetValue(ordinal); + + if (DBNull.Value.Equals(val)) + { + r.Add(dbColumn.ColumnName, null); + } + else + { + r.Add(dbColumn.ColumnName, val); + } + } + else + { + logger.LogError("Column '{Column}' of '{Table}' has no ordinal! This might introduce invalid data mapping", dbColumn.ColumnName, tableName); + } + } + + yield return r; + } + } + public IEnumerable SelectWhere(string where, params SqlParameter[] parameters) where T : ISourceModel { semanticVersion ??= SelectVersion(); diff --git a/KVA/Migration.Tool.Source/Providers/ClassMappingProvider.cs b/KVA/Migration.Tool.Source/Providers/ClassMappingProvider.cs index c5624572..3630e76a 100644 --- a/KVA/Migration.Tool.Source/Providers/ClassMappingProvider.cs +++ b/KVA/Migration.Tool.Source/Providers/ClassMappingProvider.cs @@ -1,22 +1,306 @@ +using CMS.ContentEngine; +using CMS.DataEngine; +using CMS.FormEngine; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Migration.Tool.Common; using Migration.Tool.Common.Builders; +using Migration.Tool.KXP.Api.Services.CmsClass; +using Migration.Tool.Source.Helpers; +using Migration.Tool.Source.Mappers; +using Migration.Tool.Source.Model; +using Migration.Tool.Source.Services; namespace Migration.Tool.Source.Providers; -public class ClassMappingProvider(IEnumerable classMappings) +public class ClassMappingProvider( + ILogger logger, + IEnumerable classMappings, + ModelFacade modelFacade, + ReusableSchemaService reusableSchemaService, + IFieldMigrationService fieldMigrationService, + ToolConfiguration configuration, + IEnumerable reusableSchemaBuilders) { - private readonly Dictionary mappingsByClassName = classMappings.Aggregate(new Dictionary(StringComparer.InvariantCultureIgnoreCase), - (current, sourceClassMapping) => + private readonly List configuredClassMappings = []; + private bool settingsInitialized = false; + + private Dictionary MappingsByClassName + { + get + { + var codedMappings = classMappings.Aggregate(new Dictionary(StringComparer.InvariantCultureIgnoreCase), + (current, sourceClassMapping) => + { + foreach (string s2Cl in sourceClassMapping.SourceClassNames) + { + if (!current.TryAdd(s2Cl, sourceClassMapping)) + { + throw new InvalidOperationException($"Incorrectly defined class mapping - duplicate found for class '{s2Cl}'. Fix mapping before proceeding with migration."); + } + } + + return current; + }); + + foreach (var classMapping in configuredClassMappings) + { + foreach (string classMappingSourceClassName in classMapping.SourceClassNames) + { + if (!codedMappings.TryAdd(classMappingSourceClassName, classMapping)) + { + throw new InvalidOperationException($"Duplicate class mapping '{classMapping.TargetClassName}'. (check configuration 'ConvertClassesToContentHub')"); + } + } + } + + return codedMappings; + } + } + + public IClassMapping? GetMapping(string className) + { + EnsureSettings(); + return MappingsByClassName.GetValueOrDefault(className); + } + + public Dictionary ExecuteMappings() + { + EnsureSettings(); + ExecReusableSchemaBuilders(); + + var manualMappings = new Dictionary(); + foreach (var classMapping in MappingsByClassName.Values) + { + var newDt = DataClassInfoProvider.GetDataClassInfo(classMapping.TargetClassName) ?? DataClassInfo.New(); + classMapping.PatchTargetDataClass(newDt); + + var cmsClasses = new List(); + foreach (string sourceClassName in classMapping.SourceClassNames) + { + cmsClasses.AddRange(modelFacade.SelectWhere("ClassName=@className", new SqlParameter("className", sourceClassName))); + } + + var nfi = string.IsNullOrWhiteSpace(newDt.ClassFormDefinition) ? new FormInfo() : new FormInfo(newDt.ClassFormDefinition); + bool hasPrimaryKey = false; + foreach (var formFieldInfo in nfi.GetFields(true, true, true, true, false)) + { + if (formFieldInfo.PrimaryKey) + { + hasPrimaryKey = true; + } + } + + if (!hasPrimaryKey) + { + if (string.IsNullOrWhiteSpace(classMapping.PrimaryKey)) + { + throw new InvalidOperationException($"Class mapping has no primary key set"); + } + else + { + var prototype = FormHelper.GetBasicFormDefinition(classMapping.PrimaryKey); + nfi.AddFormItem(prototype.GetFormField(classMapping.PrimaryKey)); + } + } + + newDt.ClassFormDefinition = nfi.GetXmlDefinition(); + + foreach (string schemaName in classMapping.ReusableSchemaNames) + { + reusableSchemaService.AddReusableSchemaToDataClass(newDt, schemaName); + } + + nfi = new FormInfo(newDt.ClassFormDefinition); + + var fieldInReusableSchemas = reusableSchemaService.GetFieldsFromReusableSchema(newDt).ToDictionary(x => x.Name, x => x); + + bool hasFieldsAlready = true; + foreach (var cmml in classMapping.Mappings.Where(m => m.IsTemplate).ToLookup(x => x.SourceFieldName)) + { + foreach (var cmm in cmml) + { + if (fieldInReusableSchemas.ContainsKey(cmm.TargetFieldName)) + { + // part of reusable schema + continue; + } + + var sc = cmsClasses.FirstOrDefault(sc => sc.ClassName.Equals(cmm.SourceClassName, StringComparison.InvariantCultureIgnoreCase)) + ?? throw new NullReferenceException($"The source class '{cmm.SourceClassName}' does not exist - wrong mapping {classMapping}"); + + var fi = new FormInfo(sc.ClassFormDefinition); + if (nfi.GetFormField(cmm.TargetFieldName) is { }) + { + } + else + { + var src = fi.GetFormField(cmm.SourceFieldName); + src.Name = cmm.TargetFieldName; + nfi.AddFormItem(src); + hasFieldsAlready = false; + } + } + //var cmm = cmml.FirstOrDefault() ?? throw new InvalidOperationException(); + } + + if (!hasFieldsAlready) + { + FormDefinitionHelper.MapFormDefinitionFields(logger, fieldMigrationService, nfi.GetXmlDefinition(), false, true, newDt, false, false); + CmsClassMapper.PatchDataClassInfo(newDt, out _, out _); + } + + if (classMapping.TargetFieldPatchers.Count > 0) + { + nfi = new FormInfo(newDt.ClassFormDefinition); + foreach (string fieldName in classMapping.TargetFieldPatchers.Keys) + { + classMapping.TargetFieldPatchers[fieldName].Invoke(nfi.GetFormField(fieldName)); + } + + newDt.ClassFormDefinition = nfi.GetXmlDefinition(); + } + + DataClassInfoProvider.SetDataClassInfo(newDt); + foreach (var gByClass in classMapping.Mappings.GroupBy(x => x.SourceClassName)) + { + manualMappings.TryAdd(gByClass.Key, (newDt, classMapping)); + } + + foreach (string sourceClassName in classMapping.SourceClassNames) + { + if (newDt.ClassContentTypeType is ClassContentTypeType.REUSABLE) + { + continue; + } + + var sourceClass = cmsClasses.First(c => c.ClassName.Equals(sourceClassName, StringComparison.InvariantCultureIgnoreCase)); + foreach (var cmsClassSite in modelFacade.SelectWhere("ClassId = @classId", new SqlParameter("classId", sourceClass.ClassID))) + { + if (modelFacade.SelectById(cmsClassSite.SiteID) is { SiteGUID: var siteGuid }) + { + if (ChannelInfoProvider.ProviderObject.Get(siteGuid) is { ChannelID: var channelId }) + { + var info = new ContentTypeChannelInfo { ContentTypeChannelChannelID = channelId, ContentTypeChannelContentTypeID = newDt.ClassID }; + ContentTypeChannelInfoProvider.ProviderObject.Set(info); + } + else + { + logger.LogWarning("Channel for site with SiteGUID '{SiteGuid}' not found", siteGuid); + } + } + else + { + logger.LogWarning("Source site with SiteID '{SiteId}' not found", cmsClassSite.SiteID); + } + } + } + } + + return manualMappings; + } + + private void ExecReusableSchemaBuilders() + { + foreach (var reusableSchemaBuilder in reusableSchemaBuilders) { - foreach (string s2Cl in sourceClassMapping.SourceClassNames) + reusableSchemaBuilder.AssertIsValid(); + var fieldInfos = reusableSchemaBuilder.FieldBuilders.Select(fb => { - if (!current.TryAdd(s2Cl, sourceClassMapping)) + switch (fb) { - throw new InvalidOperationException($"Incorrectly defined class mapping - duplicate found for class '{s2Cl}'. Fix mapping before proceeding with migration."); + case { Factory: { } factory }: + { + return factory(); + } + case { SourceFieldIdentifier: { } fieldIdentifier }: + { + var sourceClass = modelFacade.SelectWhere("ClassName=@className", new SqlParameter("className", fieldIdentifier.ClassName)).SingleOrDefault() + ?? throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fieldIdentifier.ClassName}': DataClass not found, class name '{fieldIdentifier.ClassName}'"); + + if (string.IsNullOrWhiteSpace(sourceClass.ClassFormDefinition)) + { + throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fieldIdentifier.ClassName}': Class '{fieldIdentifier.ClassName}' is missing field '{fieldIdentifier.FieldName}'"); + } + + // this might be cached as optimization + var patcher = new FormDefinitionPatcher( + logger, + sourceClass.ClassFormDefinition, + fieldMigrationService, + sourceClass.ClassIsForm.GetValueOrDefault(false), + sourceClass.ClassIsDocumentType, + true, + false + ); + + patcher.PatchFields(); + patcher.RemoveCategories(); + + var fi = new FormInfo(patcher.GetPatched()); + return fi.GetFormField(fieldIdentifier.FieldName) switch + { + { } field => field, + _ => throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fieldIdentifier.ClassName}': Class '{fieldIdentifier.ClassName}' is missing field '{fieldIdentifier.FieldName}'") + }; + } + default: + { + throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fb.TargetFieldName}'"); + } + } + }); + + reusableSchemaService.EnsureReusableFieldSchema(reusableSchemaBuilder.SchemaName, reusableSchemaBuilder.SchemaDisplayName, reusableSchemaBuilder.SchemaDescription, fieldInfos.ToArray()); + } + } + + private void EnsureSettings() + { + if (!settingsInitialized) + { + settingsInitialized = true; + var customTableClasses = modelFacade.Select("ClassIsCustomTable=1", "ClassID ASC").ToList(); + + foreach (string classNameForConversion in configuration.ClassNamesConvertToContentHub) + { + if (customTableClasses.FirstOrDefault(c => c.ClassName.Equals(classNameForConversion, StringComparison.InvariantCultureIgnoreCase)) is { } mappedClass) + { + var m = new MultiClassMapping(classNameForConversion, target => + { + target.ClassName = classNameForConversion; + target.ClassTableName = mappedClass.ClassTableName; + target.ClassDisplayName = mappedClass.ClassDisplayName; + target.ClassType = ClassType.CONTENT_TYPE; + target.ClassContentTypeType = ClassContentTypeType.REUSABLE; + }); + + var fi = new FormInfo(mappedClass.ClassFormDefinition); + foreach (var formFieldInfo in fi.GetFields(true, true)) + { + if (formFieldInfo.PrimaryKey) + { + m.PrimaryKey = formFieldInfo.Name; + } + else + { + m.BuildField(formFieldInfo.Name).SetFrom(mappedClass.ClassName, formFieldInfo.Name, true); + } + } + + AppendConfiguredMapping(m); } } + } + } - return current; - }); + private void AppendConfiguredMapping(IClassMapping configuredClassMapping) + { + if (classMappings.Any(cm => cm.SourceClassNames.Any(scn => string.Equals(scn, configuredClassMapping.TargetClassName, StringComparison.InvariantCultureIgnoreCase)))) + { + throw new InvalidOperationException($"Duplicate class mapping '{configuredClassMapping.TargetClassName}'. (check configuration 'ConvertClassesToContentHub')"); + } - public IClassMapping? GetMapping(string className) => mappingsByClassName.GetValueOrDefault(className); + configuredClassMappings.Add(configuredClassMapping); + } } diff --git a/KVA/Migration.Tool.Source/Providers/ContentItemNameProvider.cs b/KVA/Migration.Tool.Source/Providers/ContentItemNameProvider.cs deleted file mode 100644 index b61624ae..00000000 --- a/KVA/Migration.Tool.Source/Providers/ContentItemNameProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -using CMS.Base; -using CMS.ContentEngine.Internal; -using CMS.Helpers; - -namespace Migration.Tool.Source.Providers; - -internal class ContentItemNameProvider -{ - private readonly IContentItemNameValidator codeNameValidator; - - - /// - /// Creates a new instance of . - /// - public ContentItemNameProvider(IContentItemNameValidator codeNameValidator) => this.codeNameValidator = codeNameValidator; - - public Task Get(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name)); - } - - async Task Get(string name) - { - string codeName = ValidationHelper.GetCodeName(name, useUnicode: false); - - bool isCodeNameValid = ValidationHelper.IsCodeName(codeName); - - if (string.IsNullOrEmpty(codeName) || !isCodeNameValid) - { - codeName = TypeHelper.GetNiceName(ContentItemInfo.OBJECT_TYPE); - } - - var uniqueCodeNameProvider = new UniqueContentItemNameProvider(codeNameValidator); - - return await uniqueCodeNameProvider.GetUniqueValue(codeName); - } - - return Get(name); - } -} diff --git a/KVA/Migration.Tool.Source/Providers/ContentItemNameValidator.cs b/KVA/Migration.Tool.Source/Providers/ContentItemNameValidator.cs deleted file mode 100644 index a1974edc..00000000 --- a/KVA/Migration.Tool.Source/Providers/ContentItemNameValidator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using CMS.ContentEngine.Internal; - -namespace Migration.Tool.Source.Providers; - -internal class ContentItemNameValidator : IContentItemNameValidator -{ - /// - public bool IsUnique(string name) => IsUnique(0, name); - - - /// - public bool IsUnique(int id, string name) - { - var contentItemInfo = new ContentItemInfo { ContentItemID = id, ContentItemName = name }; - - return contentItemInfo.CheckUniqueCodeName(); - } -} diff --git a/KVA/Migration.Tool.Source/Providers/UniqueContentItemNameProvider.cs b/KVA/Migration.Tool.Source/Providers/UniqueContentItemNameProvider.cs deleted file mode 100644 index e61f0404..00000000 --- a/KVA/Migration.Tool.Source/Providers/UniqueContentItemNameProvider.cs +++ /dev/null @@ -1,38 +0,0 @@ -using CMS.Base; -using CMS.ContentEngine.Internal; - -namespace Migration.Tool.Source.Providers; - -internal class UniqueContentItemNameProvider : UniqueStringValueProviderBase -{ - private readonly IContentItemNameValidator codeNameValidator; - - - /// - /// Creates a new instance of . - /// - public UniqueContentItemNameProvider(IContentItemNameValidator codeNameValidator) - : base(TypeHelper.GetMaxCodeNameLength(ContentItemInfo.TYPEINFO.MaxCodeNameLength)) => this.codeNameValidator = codeNameValidator; - - public override Task GetUniqueValue(string inputValue) => base.GetUniqueValue(AddSuffix(inputValue)); - - - private string AddSuffix(string codeName) - { - string randomSuffix = GetRandomSuffix(); - string codeNameWithSuffix = codeName += randomSuffix; - - if (codeNameWithSuffix.Length > MaxLength) - { - int availableLength = MaxLength - randomSuffix.Length; - - codeNameWithSuffix = $"{codeName[..availableLength]}{randomSuffix}"; - } - - return codeNameWithSuffix; - } - - - /// - protected override Task IsValueUnique(string value) => Task.FromResult(codeNameValidator.IsUnique(value)); -} diff --git a/KVA/Migration.Tool.Source/Services/AssetFacade.cs b/KVA/Migration.Tool.Source/Services/AssetFacade.cs index b0a1d4ff..091d12d9 100644 --- a/KVA/Migration.Tool.Source/Services/AssetFacade.cs +++ b/KVA/Migration.Tool.Source/Services/AssetFacade.cs @@ -133,6 +133,7 @@ public async Task FromMediaFile(IMediaFile mediaFile var folder = GetAssetFolder(site); + string? contentItemSafeName = await Service.Resolve().Get($"{mediaFile.FileName}_{translatedMediaGuid}"); var contentItem = new ContentItemSimplifiedModel { CustomProperties = [], @@ -140,7 +141,7 @@ public async Task FromMediaFile(IMediaFile mediaFile ContentItemContentFolderGUID = (await EnsureFolderStructure(mediaFolder, folder))?.ContentFolderGUID ?? folder.ContentFolderGUID, IsSecured = null, ContentTypeName = LegacyMediaFileContentType.ClassName, - Name = $"{mediaFile.FileName}_{translatedMediaGuid}", + Name = contentItemSafeName, IsReusable = true, LanguageData = languageData, }; @@ -192,20 +193,18 @@ public async Task FromAttachment(ICmsAttachment atta var folder = GetAssetFolder(site); + string? contentItemSafeName = await Service.Resolve().Get($"{attachment.AttachmentGUID}_{translatedAttachmentGuid}"); var contentItem = new ContentItemSimplifiedModel { ContentItemGUID = translatedAttachmentGuid, ContentItemContentFolderGUID = (await EnsureFolderStructure(mediaFolder, folder))?.ContentFolderGUID ?? folder.ContentFolderGUID, IsSecured = null, ContentTypeName = LegacyAttachmentContentType.ClassName, - Name = $"{attachment.AttachmentGUID}_{translatedAttachmentGuid}", + Name = contentItemSafeName, IsReusable = true, LanguageData = languageData, }; - // TODO tomas.krch: 2024-09-02 append url to protocol - // urlProtocol.AppendMediaFileUrlIfNeeded(); - return contentItem; } @@ -433,8 +432,9 @@ internal static ContentFolderModel GetAssetFolder(ICmsSite site) }; } - private static readonly IUmtModel[] prerequisites = [ - LegacyMediaFileContentType, + private static readonly IUmtModel[] prerequisites = + [ + LegacyMediaFileContentType, LegacyAttachmentContentType ]; diff --git a/KVA/Migration.Tool.Source/Services/ModuleLoader.cs b/KVA/Migration.Tool.Source/Services/ModuleLoader.cs index 63c6d658..d2abc069 100644 --- a/KVA/Migration.Tool.Source/Services/ModuleLoader.cs +++ b/KVA/Migration.Tool.Source/Services/ModuleLoader.cs @@ -25,6 +25,10 @@ public async Task LoadAsync() await sourceInstanceContext.RequestSourceInstanceInfo(); } } + else + { + logger.LogWarning("Source instance API discovery feature is disabled, capabilities of Migration Tool to migrate widgets, page urls will be limited."); + } } catch (Exception ex) { diff --git a/Migration.Tool.CLI/ConfigurationExtensions.cs b/Migration.Tool.CLI/ConfigurationExtensions.cs new file mode 100644 index 00000000..ec76a745 --- /dev/null +++ b/Migration.Tool.CLI/ConfigurationExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Configuration; + +namespace Migration.Tool.CLI; +public static class ConfigurationExtensions +{ + public static IConfigurationSection GetSectionWithFallback(this IConfigurationSection section, string key, params string[] fallbackKeys) + { + var resolvedSection = section.GetSection(key); + if (!resolvedSection.Exists()) + { + foreach (string fallbackKey in fallbackKeys) + { + resolvedSection = section.GetSection(fallbackKey); + if (resolvedSection.Exists()) + { + break; + } + } + } + return resolvedSection; + } +} diff --git a/Migration.Tool.CLI/ConfigurationValidator.cs b/Migration.Tool.CLI/ConfigurationValidator.cs index cb678eaf..92b8118c 100644 --- a/Migration.Tool.CLI/ConfigurationValidator.cs +++ b/Migration.Tool.CLI/ConfigurationValidator.cs @@ -40,17 +40,17 @@ public static IEnumerable GetValidationErrors(IConfigurationR if (settings?.GetValue(ConfigurationNames.XbKConnectionString) is not null) { - yield return new ValidationMessage(ValidationMessageType.Warning, $"Configuration key '{ConfigurationNames.XbKConnectionString}' is deprecated, use 'Settings:ConnectionStrings:CMSConnectionString' instead"); + yield return new ValidationMessage(ValidationMessageType.Warning, $"Configuration key '{ConfigurationNames.XbKConnectionString}' is deprecated, use 'Settings:XbyKApiSettings:ConnectionStrings:CMSConnectionString' instead"); } - if (CheckCfgValue(settings?.GetValue(ConfigurationNames.XbKDirPath))) + if (CheckCfgValue(settings?.GetValue(ConfigurationNames.XbKDirPath)) && CheckCfgValue(settings?.GetValue(ConfigurationNames.XbyKDirPath))) { yield return new ValidationMessage(ValidationMessageType.Error, Resources.ConfigurationValidator_GetValidationErrors_TargetCmsDirPath_IsRequired); } - var targetKxpApiSettings = settings?.GetSection(ConfigurationNames.XbKApiSettings); - if (targetKxpApiSettings is null) + var targetKxpApiSettings = settings?.GetSectionWithFallback(ConfigurationNames.XbyKApiSettings, ConfigurationNames.XbKApiSettings); + if (targetKxpApiSettings?.Exists() != true) { yield return new ValidationMessage(ValidationMessageType.Error, Resources.ConfigurationValidator_GetValidationErrors_TargetKxpApiSettings_IsRequired); } diff --git a/Migration.Tool.CLI/MIGRATION_PROTOCOL_REFERENCE.md b/Migration.Tool.CLI/MIGRATION_PROTOCOL_REFERENCE.md index 2c00cea5..08bff80c 100644 --- a/Migration.Tool.CLI/MIGRATION_PROTOCOL_REFERENCE.md +++ b/Migration.Tool.CLI/MIGRATION_PROTOCOL_REFERENCE.md @@ -1,45 +1,37 @@ -## Migration Protocol Reference +# Migration Protocol Reference Running the `Migration.Tool.CLI.exe migrate` command ( see [`Migration.Tool.CLI/README.md`](/Migration.Tool.CLI/README.md)) generates a **migration protocol file**. The protocol provides information about the result of the migration, lists required manual steps, etc. -You can find the protocol file in the location specified by -the `Settings.MigrationProtocolPath` [configuration option](/Migration.Tool.CLI/README.md#Configuration). +You can find the protocol file path in the `.\Migration.Tool.CLI\appsettings.json` file under the [`MigrationProtocolPath` configuration option](/Migration.Tool.CLI/README.md#Configuration). ## Common Migration Protocol Warnings & Errors | Message ReferenceName | Severity | Description | -|-------------------------------------------------------------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ----------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | | BulkCopyColumnMismatch | Error | For performance optimization, the migration transfers certain objects using bulk SQL queries. The columns in the related database tables must match on the source and target. The migration attempts to discard data from extra columns on the source.

**Fix**: Review the tables for customizations on the source instance and revert/update the schema to match the default Kentico Xperience 13 state. | | CmsClass_CmsRootClassTypeSkip | Information | 'CMS.Root' is a special system page type and is not supported for migration.

**Fix**: No actions required. Xperience by Kentico contains a root page type by default. | | CmsTree_TreeIsLinkFromDifferentSite | Warning | The content tree contains a page that is linked from a different site. Linked pages from other sites are not supported for migration (Xperience by Kentico currently does not allow multiple sites).

**Fix**: Remove the linked page on the source instance or create a copy that is not linked from a different site. | | CmsUser_SkipAdminUser | Warning | Migration of the 'administrator' user was not processed.

**Fix**: Remove the 'administrator' user on the target instances. | | CmsUser_SkipPublicUser | Warning | Migration of the system 'public' user was not processed.

**Fix**: No actions required. All bindings (e.g. the site binding) for the 'public' user account will be mapped automatically. | -| CustomFormComponent | Warning | Logged when Page Builder content on the source instance contains a property using a custom form component (one that doesn't exist in the default installation). You need to manually create a corresponding component suitable for Xperience by Kentico on the target instance. | +| CustomFormComponent | Warning | Logged when page builder content on the source instance contains a property using a custom form component (one that doesn't exist in the default installation). You need to manually create a corresponding component suitable for Xperience by Kentico on the target instance. | | DataMustNotExistInTargetInstanceTable | Warning | Occurs when data is present for objects migrated using bulk SQL queries (for performance optimization, for example the Contact management migration).

**Fix**: You always need to delete all objects of the given types before running repeated migrations. | | DbConstraintBroken | Error | General error message indicating that the target database cannot hold migrated data, due to duplicate values in unique columns (e.g., primary keys) or due to a missing dependency.

**Fix**: Manually migrate the object (specified object by the ID in the message), or check if dependencies required by the object were successfully migrated. | | EntityExplicitlyExcludedByCodeName | Warning | The object is excluded via the `Settings.EntityConfigurations..ExcludeCodeNames` configuration option.

**Fix**: If you wish to migrate the object, remove the given code name from the configuration. | | ErrorSavingTargetInstance | Error | A general error that prevents data from being saved in the target Xperience by Kentico database. For example, can be caused by a database timeout or temporary outage. | -| FailedToCreateTargetInstance | Error | General error when the creation of the target entity failed. Can be caused by a problem with the source data or by an error in the Migration tool.

**Fix**: Check if the related source data is compatible with the target or report a Migration tool issue. | +| FailedToCreateTargetInstance | Error | General error when the creation of the target entity failed. Can be caused by a problem with the source data or by an error in the Migration tool.

**Fix**: Check if the related source data is compatible with the target or report a Migration tool issue. | | FailedToUpdateTargetInstance / FailedToUpdateTargetInstance | Error | General error when updating / inserting an object on the target instance.

**Fix**: Depends on the details in the error message. Can have various causes, such as an interrupted database connection or malformed/incorrect data. | -| FormComponentNotSupportedInLegacyMode | Warning | Logged when Page Builder content on the source instance contains a property using a form component that is not supported by the Page Builder legacy mode in Xperience by Kentico. | +| FormComponentNotSupportedInLegacyMode | Warning | Logged when page builder content on the source instance contains a property using a form component that is not supported by the Page Builder legacy mode in Xperience by Kentico. | | InvalidSourceData | Warning | The source instance is missing required data or data is malformed.

**Fix**: Review the message details and try to look for differences between the source and target database schema. | | LinkedDataAlreadyMaterializedInTargetInstance | Warning | A linked page already exists in the target instance and an update is not possible.

**Fix**: No actions required. The update is probably not required. | -| MediaFileIsMissingOnSourceFilesystem | Error | Occurs when a media file is missing from the source project's file system and the `Settings.MigrateOnlyMediaFileInfo` configuration option is `false`.

**Fix**: Manually migrate the source media file to the target instance's file system. | | -| MissingConfiguration | Error | The migrate command is missing required configuration.

**Fix**: Add required configuration according to the error message. See [`Migration.Tool.CLI/README.md`](/Migration.Tool.CLI/README.md). | +| MediaFileIsMissingOnSourceFilesystem | Error | Occurs when a media file is missing from the source project's file system and the `Settings.MigrateOnlyMediaFileInfo` configuration option is `false`.

**Fix**: Manually migrate the source media file to the target instance's file system. | | +| MissingConfiguration | Error | The migrate command is missing required configuration.

**Fix**: Add required configuration according to the error message. See [`Migration.Tool.CLI/README.md`](/Migration.Tool.CLI/README.md). | | MissingRequiredDependency | Error | An object from the source is missing a dependency required on the target instance.

**Fix**: Add the missing dependency on the source instance or delete the object completely. | -| NotCurrentlySupportedSkip | Warning | Xperience by Kentico currently does not support all types of objects from Kentico Xperience 13, so they cannot be migrated via the tool. Support may be added in future versions of Xperience by Kentico and the Migration tool.

**Fix**: Implement custom migration. | +| NotCurrentlySupportedSkip | Warning | Xperience by Kentico currently does not support all types of objects from Kentico Xperience 13, so they cannot be migrated via the tool. Support may be added in future versions of Xperience by Kentico and the Migration tool.

**Fix**: Implement custom migration. | | NotSupportedSkip | Information | Occurs for data that is not supported in Xperience by Kentico. Migration is not supported, and is not planned for upcoming versions.

**Fix**: Implement custom migration. | -| SourceEntityIsNull | Error | Probably an error while loading source data. Can be caused by malformed source data or by an error in the Migration tool.**Fix**: Check the related source data. If everything looks correct, report a Migration tool issue. | +| SourceEntityIsNull | Error | Probably an error while loading source data. Can be caused by malformed source data or by an error in the Migration tool.**Fix**: Check the related source data. If everything looks correct, report a Migration tool issue. | | SourcePageIsNotPublished | Warning | Only published pages are included in the migration. This warning occurs when a page on the source is not published.

**Fix**: Publish all pages that you wish to migrate in the source instance. | | SourceValueIsRequired | Error | A source object instance has a missing value in a required field.

**Fix**: Review the message and fill in the missing value on the source instance. | | TemporaryAttachmentMigrationIsNotSupported | Warning | Temporary page attachments are created when a file upload is not finished correctly. The migration does not include temporary attachments.

**Fix**: No actions required. If you wish to migrate the file, re-upload the attachment on the source instance to create a standard attachment. | | ValueTruncationSkip | Error | Occurs when a target database field has a lower allowed size than the source database field.

**Fix**: Truncate the value to the required size on the source instance, or omit the value completely. | - -## Submit issues - -See [`CONTRIBUTING.md`](/CONTRIBUTING.md). - -When submitting issues, please provide all available information about the problem or error. If possible, include the -command line output log file and migration protocol generated for your `Migration.Tool.CLI.exe migrate` command. diff --git a/Migration.Tool.CLI/Program.cs b/Migration.Tool.CLI/Program.cs index ccc4b5a9..942672ee 100644 --- a/Migration.Tool.CLI/Program.cs +++ b/Migration.Tool.CLI/Program.cs @@ -1,7 +1,6 @@ using System.Reflection; using MediatR; using Microsoft.Data.SqlClient; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -33,7 +32,10 @@ .Build() ; -Directory.SetCurrentDirectory(config.GetValue("Settings:XbKDirPath") ?? throw new InvalidOperationException("Settings:XbKDirPath must be set to valid directory path")); +string xbykDirPath = config.GetValue("Settings:XbyKDirPath").NullIf(ConfigurationNames.TodoPlaceholder, StringComparison.InvariantCultureIgnoreCase) ?? + config.GetValue("Settings:XbKDirPath").NullIf(ConfigurationNames.TodoPlaceholder, StringComparison.InvariantCultureIgnoreCase) ?? + throw new InvalidOperationException("Settings:XbKDirPath must be set to valid directory path"); +Directory.SetCurrentDirectory(xbykDirPath); var validationErrors = ConfigurationValidator.GetValidationErrors(config); bool anyValidationErrors = false; @@ -86,7 +88,7 @@ var settingsSection = config.GetRequiredSection(ConfigurationNames.Settings); var settings = settingsSection.Get() ?? new ToolConfiguration(); -var kxpApiSettings = settingsSection.GetSection(ConfigurationNames.XbKApiSettings); +var kxpApiSettings = settingsSection.GetSectionWithFallback(ConfigurationNames.XbyKApiSettings, ConfigurationNames.XbKApiSettings); settings.SetXbKConnectionStringIfNotEmpty(kxpApiSettings["ConnectionStrings:CMSConnectionString"]); FieldMappingInstance.PrepareFieldMigrations(settings); @@ -156,7 +158,7 @@ return; } -services.UseKxpApi(kxpApiSettings, settings.XbKDirPath); +services.UseKxpApi(kxpApiSettings, settings.XbyKDirPath ?? settings.XbKDirPath); services.AddSingleton(settings); services.AddSingleton(); services.UseToolCommon(); diff --git a/Migration.Tool.CLI/README.md b/Migration.Tool.CLI/README.md index 3862e5b6..e49650fc 100644 --- a/Migration.Tool.CLI/README.md +++ b/Migration.Tool.CLI/README.md @@ -7,21 +7,21 @@ The migration is performed by running a command for the .NET CLI. ## Set up the source instance -The source instance of Kentico must **not** use a [separated contact management database](https://docs.kentico.com/x/4giRBg), it is recommended that you [rejoin the contact management database](https://docs.kentico.com/x/5giRBg) before proceeding with the migration. +The source instance must **not** use a [separated contact management database](https://docs.kentico.com/x/4giRBg), it is recommended that you [rejoin the contact management database](https://docs.kentico.com/x/5giRBg) before proceeding with the migration. ## Set up the target instance The target of the migration must be an Xperience by Kentico instance that fulfills the following requirements: -* The instance's database and file system must be accessible from the environment where you run the migration. -* The target application *must not be running* when you start the migration. -* The target instance must be empty except for data from the source instance created by previous runs of this tool. -* For performance optimization, the migration transfers certain objects using bulk SQL queries. As a result, you always +- The instance's database and file system must be accessible from the environment where you run the migration. +- The target application _must not be running_ when you start the migration. +- The target instance must be empty except for data from the source instance created by previous runs of this tool. +- For performance optimization, the migration transfers certain objects using bulk SQL queries. As a result, you always need to delete all objects of the following types before running repeated migrations: - * **Contacts**, including their **Activities** (when using the `migrate --contact-management` parameter) - * **Consent agreements** (when using the `migrate --data-protection` parameter) - * **Form submissions** (when using the `migrate --forms` parameter) - * **Custom module class data** (when using the `--custom-modules` parameter) + - **Contacts**, including their **Activities** (when using the `migrate --contact-management` parameter) + - **Consent agreements** (when using the `migrate --data-protection` parameter) + - **Form submissions** (when using the `migrate --forms` parameter) + - **Custom module class data** (when using the `--custom-modules` parameter) To create a suitable target instance, [install a new Xperience by Kentico project](https://docs.xperience.io/x/DQKQC) using the **Boilerplate** project template. @@ -49,35 +49,35 @@ Migration.Tool.CLI.exe migrate --sites --custom-modules --users --members --form ``` | Parameter | Description | Dependencies | -|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------| +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | `--sites` | Enables migration of sites to [website channels](https://docs.xperience.io/x/34HFC). The site's basic properties and settings are transferred to the target instance. | | | `--custom-modules` | Enables migration of custom modules, [custom module classes and their data](https://docs.xperience.io/x/AKDWCQ), and [custom fields in supported system classes](https://docs.xperience.io/x/V6rWCQ).

See: [Migration details for specific object types - Custom modules and classes](#custom-modules-and-classes) | `--sites` | | `--custom-tables` | Enables migration of [custom tables](https://docs.kentico.com/x/eQ2RBg).

See: [Migration details for specific object types - Custom tables](#custom-tables) | | | `--users` | Enables migration of [users](https://docs.xperience.io/x/8ILWCQ) and [roles](https://docs.xperience.io/x/7IVwCg).

See: [Migration details for specific object types - Users](#users) | `--sites`, `--custom-modules` | | `--members` | Enables migration of live site user accounts to [members](https://docs.xperience.io/x/BIsuCw).

See: [Migration details for specific object types - Members](#members) | `--sites`, `--custom-modules` | | `--settings-keys` | Enables migration of values for [settings](https://docs.xperience.io/x/7YjFC) that are available in Xperience by Kentico. | `--sites` | -| `--page-types` | Enables migration of [content types](https://docs.xperience.io/x/gYHWCQ) (originally *page types* in Kentico Xperience 13) and [preset page templates](https://docs.xperience.io/x/KZnWCQ) (originally *custom page templates*). Required to migrate Pages.

See: [Migration details for specific object types - Content types](#content-types) | `--sites` | +| `--page-types` | Enables migration of [content types](https://docs.xperience.io/x/gYHWCQ) (originally _page types_ in Kentico Xperience 13) and [preset page templates](https://docs.xperience.io/x/KZnWCQ) (originally _custom page templates_). Required to migrate Pages.

See: [Migration details for specific object types - Content types](#content-types) | `--sites` | | `--pages` | Enables migration of [pages](https://docs.xperience.io/x/bxzfBw).

The target instance must not contain pages other than those created by previous runs of the Kentico Migration Tool.

See: [Migration details for specific object types - Pages](#pages) | `--sites`, `--users`, `--page-types` | | `--categories` | Enables migration of categories to taxonomies. Xperience by Kentico uses a different approach to categorization. Categories are migrated to [taxonomies](https://docs.kentico.com/x/taxonomies_xp) and selected categories for each page are assigned to pages in the target instance via a [reusable field schema](https://docs.kentico.com/x/D4_OD). See [`Categories`](#categories). | `--sites`, `--users`, `--pagetypes`, `--pages` | -| `--attachments` | Enables migration of page attachments to [content hub](https://docs.kentico.com/x/barWCQ) as content item assets (page attachments are not supported in Xperience by Kentico).

See: [Migration details for specific object types - Attachments](#attachments) | `--sites`, `--custom-modules` | +| `--attachments` | Enables migration of page attachments to [content hub](https://docs.kentico.com/x/barWCQ) as content item assets (page attachments are not supported in Xperience by Kentico).

See: [Migration details for specific object types - Attachments](#attachments) | `--sites`, `--custom-modules` | | `--contact-management` | Enables migration of [contacts](https://docs.xperience.io/x/nYPWCQ) and [activities](https://docs.xperience.io/x/oYPWCQ). The target instance must not contain any contacts or activities. May run for a long time depending on the number of contacts in the source database. | `--users`, `--custom-modules` | | `--data-protection` | Enables migration of [consents](https://docs.xperience.io/x/zoB1CQ) and consent agreements. | `--sites`, `--users`, `--contact management` | | `--forms` | Enables migration of [forms](https://docs.xperience.io/x/WAKiCQ) and submitted form data.

See: [Migration details for specific object types - Forms](#forms) | `--sites`, `--custom-modules`, `--users` | -| `--media-libraries` | Enables migration of [media libraries](https://docs.xperience.io/x/agKiCQ) to [content hub](https://docs.kentico.com/x/barWCQ) as content item assets. This behavior can be adjusted by `MigrateOnlyMediaFileInfo` and `MigrateMediaToMediaLibrary` [configuration options](#configuration). | `--sites`, `--custom-modules`, `--users` | +| `--media-libraries` | Enables migration of [media libraries](https://docs.xperience.io/x/agKiCQ) to [content hub](https://docs.kentico.com/x/barWCQ) as content item assets. This behavior can be adjusted by `MigrateOnlyMediaFileInfo` and `MigrateMediaToMediaLibrary` [configuration options](#configuration). | `--sites`, `--custom-modules`, `--users` | | `--countries` | Enables migration of countries and states. Xperience by Kentico currently uses countries and states to fill selectors when editing contacts and contact group conditions. | | | `--bypass-dependency-check` | Skips the migrate command's dependency check. Use for repeated runs of the migration if you know that dependencies were already migrated successfully (for example `--page types` when migrating pages). | | ### Examples -* `Migration.Tool.CLI.exe migrate --sites --custom-modules --users --settings-keys --media-libraries --page-types --pages` - * First migration that includes the site object, custom modules and classes, users, setting key values, media - libraries, page types and pages -* `Migration.Tool.CLI.exe migrate --page-types --pages --bypass-dependency-check` - * Repeated migration only for page types and pages, if you know that sites and users were already migrated - successfully. -* `Migration.Tool.CLI.exe migrate --pages --bypass-dependency-check` - * Repeated migration only for pages, if you know that page types, sites, and users were already migrated - successfully. +- `Migration.Tool.CLI.exe migrate --sites --custom-modules --users --settings-keys --media-libraries --page-types --pages` + - First migration that includes the site object, custom modules and classes, users, setting key values, media + libraries, page types and pages +- `Migration.Tool.CLI.exe migrate --page-types --pages --bypass-dependency-check` + - Repeated migration only for page types and pages, if you know that sites and users were already migrated + successfully. +- `Migration.Tool.CLI.exe migrate --pages --bypass-dependency-check` + - Repeated migration only for pages, if you know that page types, sites, and users were already migrated + successfully. ### Migration details for specific object types @@ -87,44 +87,44 @@ Content types are named **Page types** in earlier Kentico products. Xperience by Kentico currently does not support: -* Macro expressions in page type field default values or other settings. Content type fields containing macros will not +- Macro expressions in page type field default values or other settings. Content type fields containing macros will not work correctly after the migration. -* Page type inheritance. You cannot migrate page types that inherit fields from other types. -* Categories for page type fields. Field categories are not migrated with page types. +- Page type inheritance. You cannot migrate page types that inherit fields from other types. +- Categories for page type fields. Field categories are not migrated with page types. -The Kentico Migration Tool attempts to map the *Data type* and *Form control* of page type fields to an appropriate +The Kentico Migration Tool attempts to map the _Data type_ and _Form control_ of page type fields to an appropriate equivalent in Xperience by Kentico. This is not always possible, and cannot be done for custom data types or form controls. We recommend that you check your content type fields after the migration and adjust them if necessary. The following table describes how the Kentico Migration Tool maps the data types and form controls/components of page type fields: -| KX13/12/11 Data type | XbK Data type | KX13/12/11 Form control | XbK Form component | -|--------------------------|--------------------------|-------------------------|-----------------------------------------------------------------------------------------| +| KX13/12/11 Data type | XbyK Data type | KX13/12/11 Form control | XbyK Form component | +| ------------------------ | ------------------------ | ----------------------- | --------------------------------------------------------------------------------------- | | Text | Text | Text box | Text input | | Text | Text | Drop-down list | Dropdown selector | | Text | Text | Radio buttons | Radio button group | | Text | Text | Text area | Text area | -| Text | Text | *other* | Text input | +| Text | Text | _other_ | Text input | | Long text | Long text | Rich text editor | Rich text editor | | Long text | Long text | Text box | Text input | | Long text | Long text | Drop-down list | Dropdown selector | | Long text | Long text | Text area | Text area | -| Long text | Long text | *other* | Rich text editor | -| Integer number | Integer number | *any* | Number input | -| Long integer number | Long integer number | *any* | Number input | -| Floating-point number | Floating-point number | *any* | Number input | -| Decimal number | Decimal number | *any* | Decimal number input | -| Date and time | Date and time | *any* | Datetime input | -| Date | Date | *any* | Date input | -| Time interval | Time interval | *any* | None (not supported) | -| Boolean (Yes/No) | Boolean (Yes/No) | *any* | Checkbox | -| Attachments | Media files | *any* (Attachments) | Media file selector
(the [attachments](#attachments) are converted to media files) | -| File | Media files | *any* (Direct uploader) | Media file selector
(the [attachments](#attachments) are converted to media files) | -| Unique identifier (Guid) | Unique identifier (Guid) | *any* | None (not supported) | -| Pages | Pages | *any* (Pages) | Page selector | - -Additionally, you can enable the Conversion of text fields with media links (*Media selection* form control) to content item assets or media +| Long text | Long text | _other_ | Rich text editor | +| Integer number | Integer number | _any_ | Number input | +| Long integer number | Long integer number | _any_ | Number input | +| Floating-point number | Floating-point number | _any_ | Number input | +| Decimal number | Decimal number | _any_ | Decimal number input | +| Date and time | Date and time | _any_ | Datetime input | +| Date | Date | _any_ | Date input | +| Time interval | Time interval | _any_ | None (not supported) | +| Boolean (Yes/No) | Boolean (Yes/No) | _any_ | Check box | +| Attachments | Media files | _any_ (Attachments) | Media file selector
(the [attachments](#attachments) are converted to media files) | +| File | Media files | _any_ (Direct uploader) | Media file selector
(the [attachments](#attachments) are converted to media files) | +| Unique identifier (Guid) | Unique identifier (Guid) | _any_ | None (not supported) | +| Pages | Pages | _any_ (Pages) | Page selector | + +Additionally, you can enable the Conversion of text fields with media links (_Media selection_ form control) to content item assets or media library files by setting the `OptInFeatures.CustomMigration.FieldMigrations` [configuration option](#convert-text-fields-with-media-links). @@ -146,50 +146,50 @@ If the target instance is a [SaaS project](https://docs.kentico.com/x/saas_xp) ( #### Pages -* The migration includes the following versions of pages: - * _Published_ - * _Latest draft version_ - for published pages, the version is migrated to the - _Draft_ [workflow step](https://docs.xperience.io/x/JwKQC#Pages-Pageworkflow); for pages that do not have a - published version, the version is migrated to the _Draft (initial)_ workflow step. - * _Archived_ -* URLs are migrated depending on the source instance version: - * For Kentico Xperience 13, the migration: - * includes the URL paths of pages and Former URLs - * does not include Alternative URLs - * For Kentico 12 and Kentico 11, URL paths are not migrated. Instead, a default URL path is created from - the `DocumentUrlPath` or `NodeAliasPath`. -* Linked pages are currently not supported in Xperience by Kentico. The migration creates standard page copies for any +- The migration includes the following versions of pages: + - _Published_ + - _Latest draft version_ - for published pages, the version is migrated to the + _Draft_ [workflow step](https://docs.xperience.io/x/JwKQC#Pages-Pageworkflow); for pages that do not have a + published version, the version is migrated to the _Draft (initial)_ workflow step. + - _Archived_ +- URLs are migrated depending on the source instance version: + - For Kentico Xperience 13, the migration: + - includes the URL paths of pages and Former URLs + - does not include Alternative URLs + - For Kentico 12 and Kentico 11, URL paths are not migrated. Instead, a default URL path is created from + the `DocumentUrlPath` or `NodeAliasPath`. +- Linked pages are currently not supported in Xperience by Kentico. The migration creates standard page copies for any linked pages on the source instance. -* Page permissions (ACLs) are currently not migrated into Xperience by Kentico. -* Migration of Page Builder content is only available for Kentico Xperience 13. +- Page permissions (ACLs) are currently not migrated into Xperience by Kentico. +- Migration of page builder content is only available for Kentico Xperience 13. -#### Page Builder content +#### Page builder content -> :warning: Page Builder content migration is only available when migrating from Kentico Xperience 13. +> :warning: Page builder content migration is only available when migrating from Kentico Xperience 13. -By default, JSON data storing the Page Builder content of pages and custom page templates is migrated directly without +By default, JSON data storing the page builder content of pages and custom page templates is migrated directly without modifications. On the target Xperience by Kentico instance, the migrated data can work in the Page Builder's legacy compatibility mode. However, we strongly recommend updating your codebase to the new Xperience by Kentico components. -The Kentico Migration Tool provides an advanced migration mode for Page Builder content that utilizes API discovery on +The Kentico Migration Tool provides an advanced migration mode for page builder content that utilizes API discovery on the source instance. To learn more details and how to configure this feature, see [Source instance API discovery](#source-instance-api-discovery). #### Categories -Xperience by Kentico uses a different approach to categorization than older Kentico +Xperience by Kentico uses a different approach to categorization than older product versions. [Categories](https://docs.kentico.com/13/configuring-xperience/configuring-the-environment-for-content-editors/configuring-categories) were replaced by [taxonomies](https://docs.kentico.com/developers-and-admins/configuration/taxonomies) and selected categories for each page are assigned to pages in the target instance via a [reusable field schema](https://docs.kentico.com/x/D4_OD). The key differences are: -* Categories in older versions can be added to any page via the *Properties -> Categories* tab. Taxonomies can only be - used for content items (pages, emails...) that have a field with the *Taxonomy* data type. -* Categories can be global or site-specific. Taxonomies are always global, as there are no sites in Xperience by +- Categories in older versions can be added to any page via the _Properties -> Categories_ tab. Taxonomies can only be + used for content items (pages, emails...) that have a field with the _Taxonomy_ data type. +- Categories can be global or site-specific. Taxonomies are always global, as there are no sites in Xperience by Kentico. -* Categories are assigned to pages regardless of their workflow step. Taxonomies are stored as a field and are covered +- Categories are assigned to pages regardless of their workflow step. Taxonomies are stored as a field and are covered by workflow. As a result, assigned tags can be different in each workflow step. -* [Categories stored as a field](https://docs.kentico.com/x/wA_RBg) +- [Categories stored as a field](https://docs.kentico.com/x/wA_RBg) and [personal categories](https://docs.kentico.com/x/IgqRBg) are not supported by the migration. The migration process for categories performs the following steps: @@ -199,52 +199,51 @@ The migration process for categories performs the following steps: 2. A new [reusable field schema](https://docs.kentico.com/x/D4_OD) named **Categories container** (code name `categories_container`) is created to allow linking tags to pages. -* The schema contains one field, **Categories_Legacy** (data type **Taxonomy**, configured to enable selection from the - *Categories* taxonomy). + - The schema contains one field, **Categories_Legacy** (data type **Taxonomy**, configured to enable selection from the _Categories_ taxonomy). -3. On the target instance, the *Categories container* reusable field schema is added to all content types where at least +3. On the target instance, the _Categories container_ reusable field schema is added to all content types where at least one page had a category assigned in the source instance. -4. Supported categories from the source instance are migrated as tags to the *Categories* taxonomy in the target +4. Supported categories from the source instance are migrated as tags to the _Categories_ taxonomy in the target instance. The category hierarchy from the source instance is maintained in the target instance. 5. On the target instance, tags are assigned to pages according to the source instance. -* Each [language variant](https://docs.kentico.com/business-users/website-content/translate-pages) of a page is treated +- Each [language variant](https://docs.kentico.com/business-users/website-content/translate-pages) of a page is treated individually and receives its corresponding group of tags based on the source instance. -* Tags from the source page are added to all +- Tags from the source page are added to all available [workflow steps](https://docs.kentico.com/developers-and-admins/configuration/workflows) of the target page. #### Custom modules and classes The migration includes the following: -* Custom modules - * Note: The `CMS.` prefix/namespace is reserved for system modules and not allowed in custom module code names. If - present, this code name prefix is removed during the migration. -* All classes belonging under custom modules -* All data stored within custom module classes -* The following customizable system classes and their custom fields: - * *Membership > User* - * *Media libraries > Media file* - * *Contact management > Contact management - Account* (however, accounts are currently not supported in Xperience by - Kentico) - * *Contact management > Contact management - Contact* +- Custom modules + - Note: The `CMS.` prefix/namespace is reserved for system modules and not allowed in custom module code names. If + present, this code name prefix is removed during the migration. +- All classes belonging under custom modules +- All data stored within custom module classes +- The following customizable system classes and their custom fields: + - _Membership > User_ + - _Media libraries > Media file_ + - _Contact management > Contact management - Account_ (however, accounts are currently not supported in Xperience by + Kentico) + - _Contact management > Contact management - Contact_ Module and class migration does NOT include: -* UI elements and all related user interface settings. The administration of Xperience by Kentico uses a different +- UI elements and all related user interface settings. The administration of Xperience by Kentico uses a different technology stack than Kentico Xperience 13 and is incompatible. To learn how to build the administration UI, see [Extend the administration interface](https://docs.xperience.io/x/GwKQC) and [Example - Offices management application](https://docs.xperience.io/x/hIFwCg). -* Alternative forms under classes and UI-related configuration of class fields (field labels, Form controls, etc.). You +- Alternative forms under classes and UI-related configuration of class fields (field labels, Form controls, etc.). You need to manually create appropriate [UI forms](https://docs.xperience.io/x/V6rWCQ) in Xperience by Kentico after the migration. -* Custom settings under modules, which are currently not supported in Xperience by Kentico -* Module permissions (permissions work differently in Xperience by Kentico, +- Custom settings under modules, which are currently not supported in Xperience by Kentico +- Module permissions (permissions work differently in Xperience by Kentico, see [Role management](https://docs.xperience.io/x/7IVwCg) and [UI page permission checks](https://docs.xperience.io/x/8IKyCg)) As with all object types, the Kentico Migration Tool does not transfer code files to the target project. You need to -manually move all code files generated for your custom classes (*Info*, *InfoProvider*, etc.). +manually move all code files generated for your custom classes (_Info_, _InfoProvider_, etc.). To learn more about custom modules and classes in Xperience by Kentico, see the [Object types](https://docs.xperience.io/x/AKDWCQ) documentation. @@ -253,14 +252,14 @@ the [Object types](https://docs.xperience.io/x/AKDWCQ) documentation. The migration includes the following: -* Basic information about custom tables (from the `CMS_Class` table) is migrated to the custom module +- Basic information about custom tables (from the `CMS_Class` table) is migrated to the custom module table (`CMS_Resource`) as a special `customtables` resource. -* Content of individual custom tables is migrated as module classes. +- Content of individual custom tables is migrated as module classes. Custom table migration does NOT include: -* Any other data related to custom tables (queries, alternative forms) are discarded by the migration. -* UI elements related to custom tables such as listings and filters are not migrated and need to be implemented. The +- Any other data related to custom tables (queries, alternative forms) are discarded by the migration. +- UI elements related to custom tables such as listings and filters are not migrated and need to be implemented. The administration of Xperience by Kentico uses a different technology stack than Kentico Xperience 13 and is incompatible. To learn how to build the administration UI, see [Extend the administration interface](https://docs.xperience.io/x/GwKQC) @@ -268,13 +267,13 @@ Custom table migration does NOT include: #### Media libraries -Media library files are migrated as content item assets to the [content hub](https://docs.kentico.com/x/barWCQ) into a content folder `/`. All assets are created in the default language of the respective site. Migrated assets are created as content items of a *Legacy media file* content type (code name `Legacy.Mediafile`) created by the tool. +Media library files are migrated as content item assets to the [content hub](https://docs.kentico.com/x/barWCQ) into a content folder `/`. All assets are created in the default language of the respective site. Migrated assets are created as content items of a _Legacy media file_ content type (code name `Legacy.Mediafile`) created by the tool. If required, you can [configure the tool](#convert-attachments-and-media-library-files-to-media-libraries-instead-of-content-item-assets) to instead migrate media libraries as media libraries on the target instance. #### Attachments -Attachment files are migrated as content item assets to the [content hub](https://docs.kentico.com/x/barWCQ) into a content folder `/__Attachments`. Assets are created in the specified language if the language is available (e.g., attachments of pages). Migrated assets are created as content items of a *Legacy attachment* content type (code name `Legacy.Attachment`) created by the tool. +Attachment files are migrated as content item assets to the [content hub](https://docs.kentico.com/x/barWCQ) into a content folder `/__Attachments`. Assets are created in the specified language if the language is available (e.g., attachments of pages). Migrated assets are created as content items of a _Legacy attachment_ content type (code name `Legacy.Attachment`) created by the tool. If required, you can [configure the tool](#convert-attachments-and-media-library-files-to-media-libraries-instead-of-content-item-assets) to instead migrate attachments as media libraries on the target instance. @@ -287,24 +286,24 @@ templates and Emails. See [Emails](https://docs.xperience.io/x/IaDWCQ). #### Users -**Note**: Xperience by Kentico uses separate entities for users with access to the administration interface (*CMS_User* -table) and live site visitor accounts (*CMS_Member* table). Consequently, only users whose *Privilege level* is set to -*Editor* and above are migrated (*Users* -> edit a user -> *General* tab) via the `--users` command. To migrate live +**Note**: Xperience by Kentico uses separate entities for users with access to the administration interface (_CMS_User_ +table) and live site visitor accounts (_CMS_Member_ table). Consequently, only users whose _Privilege level_ is set to +_Editor_ and above are migrated (_Users_ -> edit a user -> _General_ tab) via the `--users` command. To migrate live site accounts as well, use [`--members`](#migrate-command-parameters). The command migrates all users with access to the administration interface. Note the following expected behavior: -* The 'administrator' user account is only transferred from the source if it does not exist on the target instance. -* The 'public' system user is updated, and all bindings (e.g., the site binding) are mapped automatically on the target +- The 'administrator' user account is only transferred from the source if it does not exist on the target instance. +- The 'public' system user is updated, and all bindings (e.g., the site binding) are mapped automatically on the target instance. -* Site bindings are updated automatically for all migrated users. -* Users in Xperience by Kentico must have an email address. Migration is only supported for users with a **unique** +- Site bindings are updated automatically for all migrated users. +- Users in Xperience by Kentico must have an email address. Migration is only supported for users with a **unique** email address on the source instance. - * If you encounter issues related to email validation, you can change the default validation behavior via - the `CMSEmailValidationRegex` [application key](https://docs.xperience.io/x/yA6RBg). -* Custom user fields can be migrated together with *module classes*. + - If you encounter issues related to email validation, you can change the default validation behavior via + the `CMSEmailValidationRegex` [application key](https://docs.xperience.io/x/yA6RBg). +- Custom user fields can be migrated together with _module classes_. -Additionally, the command migrates all roles and user-role bindings for users whose *Privilege level* is *Editor* or +Additionally, the command migrates all roles and user-role bindings for users whose _Privilege level_ is _Editor_ or higher. Because Xperience by Kentico uses a different [permission model](https://docs.xperience.io/x/7IVwCg), no existing role @@ -314,22 +313,22 @@ configured again. #### Members In Xperience by Kentico, live site users are represented using a separate **Member** entity and stored in the -*CMS_Member* table. +_CMS_Member_ table. The migration identifies live site users as those without access to the administration interface. That is, only those -accounts whose *Privilege level* is set to *None* (Users -> edit a user -> General tab) are migrated. +accounts whose _Privilege level_ is set to _None_ (Users -> edit a user -> General tab) are migrated. The migration includes: -* All system fields from the *CMS_User* and *CMS_UserSettings* tables. You can customize which fields are migrated via +- All system fields from the _CMS_User_ and _CMS_UserSettings_ tables. You can customize which fields are migrated via the `MemberIncludeUserSystemFields` configuration option. See [configuration](#configuration). -* All custom fields added to the *CMS_User* and *CMS_UserSettings* tables are migrated under `CMS_Member`. The columns specified in the `MemberIncludeSystemFields` option are appended to the `CMS_Member` table in the order in which they were specified. +- All custom fields added to the _CMS_User_ and _CMS_UserSettings_ tables are migrated under `CMS_Member`. The columns specified in the `MemberIncludeSystemFields` option are appended to the `CMS_Member` table in the order in which they were specified. As an example, take the following `CMS_Member` columns ```text |MemberId|MemberEmail|...|MemberSecurityStamp| ``` - + And the following `Migration.Tool.CLI/appsettings.json` configuration. ```json @@ -339,22 +338,22 @@ The migration includes: ``` This will result in the following `CMS_Member` structure after migration. - + ```text |MemberId|MemberEmail|...|MemberSecurityStamp|FirstName|LastName|UserPrivilegeLevel|` ``` - + > If you are migrating custom fields, the `--custom-modules` migration command must be run before the `--members` - command. For example: + > command. For example: ```powershell Migration.Tool.CLI.exe migrate --sites --custom-modules --users --members ``` -The migration ***DOES NOT*** include: +The migration **_DOES NOT_** include: -* External login information associated with each account (e.g., Google or Facebook logins). -* User password hashes from the `CMS_User.UserPassword` column. +- External sign-in information associated with each account (e.g., Google or Facebook logins). +- User password hashes from the `CMS_User.UserPassword` column. After the migration, the corresponding `CMS_Member.MemberPassword` in the target Xperience by Kentico instance is `NULL`. This means that the migrated accounts **CANNOT** be used to sign in to the system under any circumstances. @@ -363,35 +362,35 @@ The migration ***DOES NOT*** include: See [Forms authentication](https://docs.xperience.io/x/t4ouCw) for a sample password reset process that can be adapted for this scenario. The general flow consists of these steps: - 1. Select the migrated member accounts. - - ```csharp - // Selects members whose password is null and who don't use external providers to sign in - var migratedMembers = - MemberInfo.Provider - .Get() - .WhereNull("MemberPassword") - .WhereEquals("MemberIsExternal", 0); - ``` - - 2. Generate password reset tokens for each account using `UserManager.GeneratePasswordResetTokenAsync(member)`. - 3. Send the password reset email to each account using `IEmailService`. - - ```csharp - await emailService - .SendEmail(new EmailMessage() - { - Recipients = member.Email, - Subject = "Password reset request", - // {resetURL} targets a controller action with the password reset form - Body = $"To reset your account's password, click here." - }); - ``` + 1. Select the migrated member accounts. + + ```csharp + // Selects members whose password is null and who don't use external providers to sign in + var migratedMembers = + MemberInfo.Provider + .Get() + .WhereNull("MemberPassword") + .WhereEquals("MemberIsExternal", 0); + ``` + + 2. Generate password reset tokens for each account using `UserManager.GeneratePasswordResetTokenAsync(member)`. + 3. Send the password reset email to each account using `IEmailService`. + + ```csharp + await emailService + .SendEmail(new EmailMessage() + { + Recipients = member.Email, + Subject = "Password reset request", + // {resetURL} targets a controller action with the password reset form + Body = $"To reset your account's password, click here." + }); + ``` #### Contacts -* Custom contact fields can be migrated together with *module classes*. -* For performance reasons, contacts and related objects are migrated using bulk SQL queries. As a result, you always +- Custom contact fields can be migrated together with _module classes_. +- For performance reasons, contacts and related objects are migrated using bulk SQL queries. As a result, you always need to delete all Contacts, Activities and Consent agreements before running the migration (when using the `migrate --contact-management` parameter). @@ -401,24 +400,24 @@ Before you run the migration, configure options in the `Migration.Tool.CLI/appse Add the options under the `Settings` section in the configuration file. -| Configuration | Description | -|-------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| KxConnectionString | The connection string to the source Kentico Xperience 13, Kentico 12, or Kentico 11 database. | -| KxCmsDirPath | The absolute file system path of the **CMS** folder in the source Kentico Xperience 13, Kentico 12, or Kentico 11 administration project. Required to migrate media library files. | -| XbKDirPath | The absolute file system path of the root of the target Xperience by Kentico project. Required to migrate media library and page attachment files. | -| XbKApiSettings | Configuration options set for the API when creating migrated objects in the target application.

The `ConnectionStrings.CMSConnectionString`option is required - set the connection string to the target Xperience by Kentico database (the same value as `XbKConnectionString`). | -| MigrationProtocolPath | The absolute file system path of the location where the [migration protocol file](./MIGRATION_PROTOCOL_REFERENCE.md) is generated.

For example: `"C:\\Logs\\Migration.Tool.Protocol.log"` | -| MigrateOnlyMediaFileInfo | If set to `true`, only the database representations of media files are migrated, without the files in the media folder in the project's file system. For example, enable this option if your media library files are mapped to a shared directory or Cloud storage.

If `false`, media files are migrated based on the `KxCmsDirPath` location. | -| MigrateMediaToMediaLibrary | Determines whether media library files and attachments from the source instance are migrated to the target instance as media libraries or as [content item assets](https://docs.kentico.com/x/barWCQ) in the content hub. The default value is `false` – media files and attachments are migrated as content item assets.

See [Convert attachments and media library files to media libraries instad of content item assets](#convert-attachments-and-media-library-files-to-media-libraries-instead-of-content-item-assets) | -| MemberIncludeUserSystemFields | Determines which system fields from the *CMS_User* and *CMS_UserSettings* tables are migrated to *CMS_Member* in Xperience by Kentico. Fields that do not exist in *CMS_Member* are automatically created.

The sample `appsettings.json` file included with the tool by default includes all user fields that can be migrated from Kentico Xperience 13. Exclude specific fields from the migration by removing them from this configuration option. | -| UseOmActivityNodeRelationAutofix | Determines how the migration handles references from Contact management activities to non-existing pages.

Possible options:
`DiscardData` - faulty references are removed,
`AttemptFix` - references are updated to the IDs of corresponding pages created by the migration,
`Error` - an error is reported and the reference can be translated or otherwise handled manually | -| UseOmActivitySiteRelationAutofix | Determines how the migration handles site references from Contact management activities.

Possible options: `DiscardData`,`AttemptFix`,`Error` | -| EntityConfigurations | Contains options that allow you to fine-tune the migration of specific object types. | -| EntityConfigurations.*<object table name>*.ExcludeCodeNames | Excludes objects with the specified code names from the migration. | -| CreateReusableFieldSchemaForClasses | Specifies which page types are also converted to [reusable field schemas](#convert-page-types-to-reusable-field-schemas). | -| OptInFeatures.QuerySourceInstanceApi.Enabled | If `true`, [source instance API discovery](#source-instance-api-discovery) is enabled to allow advanced migration of Page Builder content for pages and page templates. | -| OptInFeatures.QuerySourceInstanceApi.Connections | To use [source instance API discovery](#source-instance-api-discovery), you need to add a connection JSON object containing the following values:
`SourceInstanceUri` - the base URI where the source instance's live site application is running.
`Secret` - the secret that you set in the *ToolkitApiController.cs* file on the source instance. | -| OptInFeatures.CustomMigration.FieldMigrations | Enables conversion of media selection text fields to content item assets or media library files. See [Convert text fields with media links](#convert-text-fields-with-media-links) for more information. | +| Configuration | Description | +| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| KxConnectionString | The connection string to the source Kentico Xperience 13, Kentico 12, or Kentico 11 database. | +| KxCmsDirPath | The absolute file system path of the **CMS** folder in the source Kentico Xperience 13, Kentico 12, or Kentico 11 administration project. Required to migrate media library files. | +| XbyKDirPath | The absolute file system path of the root of the target Xperience by Kentico project. Required to migrate media library and page attachment files. | +| XbyKApiSettings | Configuration options set for the API when creating migrated objects in the target application.

The `ConnectionStrings.CMSConnectionString`option is required - set the connection string to the target Xperience by Kentico database (the same value as obsolete `XbKConnectionString`). | +| MigrationProtocolPath | The absolute file system path of the location where the [migration protocol file](./MIGRATION_PROTOCOL_REFERENCE.md) is generated.

For example: `"C:\\Logs\\Migration.Tool.Protocol.log"` | +| MigrateOnlyMediaFileInfo | If set to `true`, only the database representations of media files are migrated, without the files in the media folder in the project's file system. For example, enable this option if your media library files are mapped to a shared directory or Cloud storage.

If `false`, media files are migrated based on the `KxCmsDirPath` location. | +| MigrateMediaToMediaLibrary | Determines whether media library files and attachments from the source instance are migrated to the target instance as media libraries or as [content item assets](https://docs.kentico.com/x/barWCQ) in the content hub. The default value is `false` – media files and attachments are migrated as content item assets.

See [Convert attachments and media library files to media libraries instad of content item assets](#convert-attachments-and-media-library-files-to-media-libraries-instead-of-content-item-assets) | +| MemberIncludeUserSystemFields | Determines which system fields from the _CMS_User_ and _CMS_UserSettings_ tables are migrated to _CMS_Member_ in Xperience by Kentico. Fields that do not exist in _CMS_Member_ are automatically created.

The sample `appsettings.json` file included with the tool by default includes all user fields that can be migrated from Kentico Xperience 13. Exclude specific fields from the migration by removing them from this configuration option. | +| UseOmActivityNodeRelationAutofix | Determines how the migration handles references from Contact management activities to non-existing pages.

Possible options:
`DiscardData` - faulty references are removed,
`AttemptFix` - references are updated to the IDs of corresponding pages created by the migration,
`Error` - an error is reported and the reference can be translated or otherwise handled manually | +| UseOmActivitySiteRelationAutofix | Determines how the migration handles site references from Contact management activities.

Possible options: `DiscardData`,`AttemptFix`,`Error` | +| EntityConfigurations | Contains options that allow you to fine-tune the migration of specific object types. | +| EntityConfigurations._<object table name>_.ExcludeCodeNames | Excludes objects with the specified code names from the migration. | +| CreateReusableFieldSchemaForClasses | Specifies which page types are also converted to [reusable field schemas](#convert-page-types-to-reusable-field-schemas). | +| OptInFeatures.QuerySourceInstanceApi.Enabled | If `true`, [source instance API discovery](#source-instance-api-discovery) is enabled to allow advanced migration of page builder content for pages and page templates. | +| OptInFeatures.QuerySourceInstanceApi.Connections | To use [source instance API discovery](#source-instance-api-discovery), you need to add a connection JSON object containing the following values:
`SourceInstanceUri` - the base URI where the source instance's live site application is running.
`Secret` - the secret that you set in the _ToolkitApiController.cs_ file on the source instance. | +| OptInFeatures.CustomMigration.FieldMigrations | Enables conversion of media selection text fields to content item assets or media library files. See [Convert text fields with media links](#convert-text-fields-with-media-links) for more information. | ### Example @@ -440,47 +439,48 @@ Add the options under the `Settings` section in the configuration file. "Settings": { "KxConnectionString": "Data Source=myserver;Initial Catalog=Xperience13;Integrated Security=True;Persist Security Info=False;Connect Timeout=120;Encrypt=False;Current Language=English;", "KxCmsDirPath": "C:\\inetpub\\wwwroot\\Xperience13\\CMS", - "XbKDirPath": "C:\\inetpub\\wwwroot\\XP_Target", - "XbKApiSettings": { + "XbyKDirPath": "C:\\inetpub\\wwwroot\\XP_Target", + "XbyKApiSettings": { "ConnectionStrings": { "CMSConnectionString": "Data Source=myserver;Initial Catalog=XperienceByKentico;Integrated Security=True;Persist Security Info=False;Connect Timeout=120;Encrypt=False;Current Language=English;" } }, - "MigrationProtocolPath": "C:\\_Development\\xperience-migration-toolkit-master\\Migration.Toolkit.Protocol.log", - "MemberIncludeUserSystemFields": "FirstName|MiddleName|LastName|FullName|UserPrivilegeLevel|UserIsExternal|LastLogon|UserLastModified|UserGender|UserDateOfBirth", - "MigrateOnlyMediaFileInfo": false, - "MigrateMediaToMediaLibrary": false, - "UseOmActivityNodeRelationAutofix": "AttemptFix", - "UseOmActivitySiteRelationAutofix": "AttemptFix", - "EntityConfigurations": { - "CMS_Site": { - "ExplicitPrimaryKeyMapping": { - "SiteID": { - "1": 1 + "MigrationProtocolPath": "C:\\_Development\\xperience-migration-toolkit-master\\Migration.Toolkit.Protocol.log", + "MemberIncludeUserSystemFields": "FirstName|MiddleName|LastName|FullName|UserPrivilegeLevel|UserIsExternal|LastLogon|UserLastModified|UserGender|UserDateOfBirth", + "MigrateOnlyMediaFileInfo": false, + "MigrateMediaToMediaLibrary": false, + "UseOmActivityNodeRelationAutofix": "AttemptFix", + "UseOmActivitySiteRelationAutofix": "AttemptFix", + "EntityConfigurations": { + "CMS_Site": { + "ExplicitPrimaryKeyMapping": { + "SiteID": { + "1": 1 + } } + }, + "CMS_Class": { + "ExcludeCodeNames": [ + "CMS.File", + "CMS.MenuItem", + "ACME.News", + "ACME.Office", + "CMS.Blog", + "CMS.BlogPost" + ] + }, + "CMS_SettingsKey": { + "ExcludeCodeNames": ["CMSHomePagePath"] } }, - "CMS_Class": { - "ExcludeCodeNames": [ - "CMS.File", - "CMS.MenuItem", - "ACME.News", - "ACME.Office", - "CMS.Blog", - "CMS.BlogPost" - ] - }, - "CMS_SettingsKey": { - "ExcludeCodeNames": [ - "CMSHomePagePath" - ] - } - }, - "OptInFeatures":{ - "QuerySourceInstanceApi": { - "Enabled": true, + "OptInFeatures": { + "QuerySourceInstanceApi": { + "Enabled": true, "Connections": [ - { "SourceInstanceUri": "http://localhost:60527", "Secret": "__your secret string__" } + { + "SourceInstanceUri": "http://localhost:60527", + "Secret": "__your secret string__" + } ] }, "FieldMigrations": { @@ -488,7 +488,7 @@ Add the options under the `Settings` section in the configuration file. "TargetDataType": "assets", "SourceFormControl": "MediaSelectionControl", "TargetFormComponent": "Kentico.Administration.AssetSelector", - "Actions": [ "convert to asset" ], + "Actions": ["convert to asset"], "FieldNameRegex": ".*" } } @@ -500,8 +500,8 @@ Add the options under the `Settings` section in the configuration file. > :warning: **Warning** – source instance API discovery is only available when migrating from Kentico Xperience 13. -By default, JSON data storing the Page Builder content of pages and custom page templates is migrated directly without -modifications. Within this content, Page Builder components (widgets, sections, etc.) with properties have their +By default, JSON data storing the page builder content of pages and custom page templates is migrated directly without +modifications. Within this content, page builder components (widgets, sections, etc.) with properties have their configuration based on Kentico Xperience 13 form components, which are assigned to the properties on the source instance. On the target Xperience by Kentico instance, the migrated data can work in the Page Builder's legacy compatibility mode. @@ -509,23 +509,23 @@ compatibility mode. However, we strongly recommend updating your codebase to the new Xperience by Kentico components. See [Editing components in Xperience by Kentico](https://docs.xperience.io/x/wIfWCQ) to learn more. -To convert Page Builder data to a format suitable for the Xperience by Kentico components, the Kentico Migration Tool +To convert page builder data to a format suitable for the Xperience by Kentico components, the Kentico Migration Tool provides an advanced migration mode that utilizes API discovery on the source instance. The advanced mode currently provides the following data conversion: -* **Attachment selector** properties - converted to a format suitable for the Xperience by Kentico **Media selector** +- **Attachment selector** properties - converted to a format suitable for the Xperience by Kentico **Media selector** component, with `IEnumerable` values. -* **Page selector** properties - converted to a format suitable for the Xperience by Kentico Page selector component, +- **Page selector** properties - converted to a format suitable for the Xperience by Kentico Page selector component, with `IEnumerable` values. ### Prerequisites and Limitations -* To use source instance API discovery, the live site application of your source instance must be running and available +- To use source instance API discovery, the live site application of your source instance must be running and available during the migration. -* Using the advanced Page Builder data migration **prevents the data from being used in the Page Builder's legacy - compatibility mode**. With this approach, you need to update all Page Builder component code files to +- Using the advanced page builder data migration **prevents the data from being used in the Page Builder's legacy + compatibility mode**. With this approach, you need to update all page builder component code files to the [Xperience by Kentico format](https://docs.xperience.io/x/wIfWCQ). -* The source instance API discovery feature only processes component properties defined using `[EditingComponent]` +- The source instance API discovery feature only processes component properties defined using `[EditingComponent]` attribute notation. Other implementations, such as properties edited via custom view components in the Razer view, are not supported. @@ -546,78 +546,78 @@ public class MyWidgetProperties : IWidgetProperties 1. Copy the `ToolApiController.cs` file to the `Controllers` folder in the **live site project** of your Kentico Xperience 13 source instance. Get the file from the following location in the Kentico Migration Tool repository: - * For .NET Core projects: `KX13.Extensions\ToolApiController.cs` - * For MVC 5 (.NET Framework 4.8) projects: `KX13.NET48.Extensions\ToolApiController.NET48.cs` + - For .NET Core projects: `KX13.Extensions\ToolApiController.cs` + - For MVC 5 (.NET Framework 4.8) projects: `KX13.NET48.Extensions\ToolApiController.NET48.cs` 2. Register routes for the `ToolApi` controller's actions into the source instance's live site application. - * For .NET Core projects, add endpoints in the project's `Startup.cs` or `Program.cs` file: - - ```csharp - app.UseEndpoints(endpoints => - { - endpoints.MapControllerRoute( - name: "ToolExtendedFeatures", - pattern: "{controller}/{action}", - constraints: new - { - controller = "ToolApi" - } - ); - - // other routes ... - }); - ``` - - * For MVC 5 projects, map the routes in your application's `RouteCollection` (e.g., in - the `/App_Start/RouteConfig.cs` file): - - ```csharp - public static void RegisterRoutes(RouteCollection routes) - { - // Maps routes for Xperience handlers and enabled features - routes.Kentico().MapRoutes() - - routes.MapRoute( - name: "ToolExtendedFeatures", - url: "{controller}/{action}", - defaults: new { }, - constraints: new - { - controller = "ToolApi" - } - ); - - // other routes ... - } - ``` + - For .NET Core projects, add endpoints in the project's `Startup.cs` or `Program.cs` file: + + ```csharp + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "ToolExtendedFeatures", + pattern: "{controller}/{action}", + constraints: new + { + controller = "ToolApi" + } + ); + + // other routes ... + }); + ``` + + - For MVC 5 projects, map the routes in your application's `RouteCollection` (e.g., in + the `/App_Start/RouteConfig.cs` file): + + ```csharp + public static void RegisterRoutes(RouteCollection routes) + { + // Maps routes for Xperience handlers and enabled features + routes.Kentico().MapRoutes() + + routes.MapRoute( + name: "ToolExtendedFeatures", + url: "{controller}/{action}", + defaults: new { }, + constraints: new + { + controller = "ToolApi" + } + ); + + // other routes ... + } + ``` 3. Edit `ToolApiController.cs` and set a value for the `Secret` constant: - ```csharp - private const string Secret = "__your secret string__"; - ``` + ```csharp + private const string Secret = "__your secret string__"; + ``` 4. Configure the `Settings.OptInFeatures.QuerySourceInstanceApi` [configuration options](#configuration) for the - Migration Tool: - - ```json - "OptInFeatures":{ - "QuerySourceInstanceApi": { - "Enabled": true, - "Connections": [ - { "SourceInstanceUri": "http://localhost:60527", "Secret": "__your secret string__" } - ] - } - }, - ``` + Kentico Migration Tool: + + ```json + "OptInFeatures":{ + "QuerySourceInstanceApi": { + "Enabled": true, + "Connections": [ + { "SourceInstanceUri": "http://localhost:60527", "Secret": "__your secret string__" } + ] + } + }, + ``` You can test the source instance API discovery by making a POST request to `/ToolApi/Test` with `{ "secret":"__your secret string__" }` in the body. If your setup is correct, the response should be: `{ "pong": true }` -When you now [migrate data](#migrate-data), the tool performs API discovery of Page Builder component code on the source -instance and advanced migration of Page Builder data. +When you now [migrate data](#migrate-data), the tool performs API discovery of page builder component code on the source +instance and advanced migration of page builder data. ## Convert page types to reusable field schemas @@ -636,12 +636,11 @@ The following example specifies two page types from which reusable schemas are c }, ``` -> :warning: **Notes** -> -> * Conversion of page types to reusable field schemas works best when all field names of page types are unique (i.e., - prefixed with the page type name). If multiple page types converted to reusable field schemas have fields with the - same code name, the code name is prefixed with the content type name in the converted reusable field schemas. -> * Page types specified by this configuration option are also migrated as content types into to the target instance. +### :warning: Notes + +- Conversion of page types to reusable field schemas works best when all field names of page types are unique (i.e., prefixed with the page type name). If multiple page types converted to reusable field schemas have fields with the same code name, the code name is prefixed with the content type name in the converted reusable field schemas. + +- Page types specified by this configuration option are also migrated as content types into to the target instance. ## Convert text fields with media links @@ -650,18 +649,34 @@ selection_ [form control](https://docs.xperience.io/x/0A_RBg) from the source in fields in the target instance. You can instead configure the Kentico Migration Tool to convert these fields to the _Content items_ data type and use the _Content item selector_ form component, or _Media files_ data type and use the _Media file selector_ form component if you choose to [convert attachments and media library files to media libraries instead of content item assets](#convert-attachments-and-media-library-files-to-media-libraries-instead-of-content-item-assets). -> :warning: **Notes** -> -> * Only media libraries using the **Permanent** [file URL format](https://docs.xperience.io/x/xQ_RBg) are supported. - Content from media libraries with enabled **Use direct path for files in content** setting will not be converted. -> * If you enable this feature, you also need to change retrieval and handling of affected files in your code, as the - structure of the stored data changes from a text path ( - e.g.,`~/getmedia/CCEAD0F0-E2BF-459B-814A-36699E5C773E/somefile.jpeg?width=300&height=100`) to a _Media files_ data - type (internally stored as - e.g., `[{"Identifier":"CCEAD0F0-E2BF-459B-814A-36699E5C773E","Some file":"somefile.jpeg","Size":11803,"Dimensions":{"Width":300,"Height":100}}]`). - The value of the field now needs to be [retrieved as a media library file](https://docs.xperience.io/x/LA2RBg). -> * If the target instance is a [SaaS project](https://docs.kentico.com/x/saas_xp), you need to manually move content - item asset files. See [Content items](#content-items) for more information. +### :warning: Notes + +- Only media libraries using the **Permanent** [file URL format](https://docs.xperience.io/x/xQ_RBg) are supported. Content from media libraries with enabled **Use direct path for files in content** setting will not be converted. + +- If you enable this feature, you also need to change retrieval and handling of affected files in your code, as the structure of the stored data changes from a text path to a _Media files_ data type. + + Source + + ```text + ~/getmedia/CCEAD0F0-E2BF-459B-814A-36699E5C773E/somefile.jpeg?width=300&height=100 + ``` + + Destination + + ```json + [ + { + "Identifier": "CCEAD0F0-E2BF-459B-814A-36699E5C773E", + "Name": "somefile.jpeg", + "Size": 11803, + "Dimensions": { "Width": 300, "Height": 100 } + } + ] + ``` + +- The value of the field now needs to be [retrieved as a media library file](https://docs.xperience.io/x/LA2RBg). + +- If the target instance is a [SaaS project](https://docs.kentico.com/x/saas_xp), you need to manually move content item asset files. See [Content items](#content-items) for more information. ### Convert to content item assets @@ -692,9 +707,9 @@ match the regular expressions are converted. Use `.*` to match all fields. ### Convert to media libraries -* Attachment links (containing a `getattachment` handler) are migrated as [attachments](#attachments) and changed to the +- Attachment links (containing a `getattachment` handler) are migrated as [attachments](#attachments) and changed to the _Media files_ data type. -* Media file links (containing a `getmedia` handler) are changed to the _Media files_ data type. It is expected that the +- Media file links (containing a `getmedia` handler) are changed to the _Media files_ data type. It is expected that the media library containing the targeted file has been migrated. To enable this feature, configure the `OptInFeatures.CustomMigration.FieldMigrations` [options](#configuration) for this @@ -726,31 +741,31 @@ By default, media libraries and attachments are migrated as content item assets ### Media libraries -* In Xperience by Kentico, Media libraries are global instead of site-specific. -* The code name of each media library on the target instance is `{SiteName}_{LibraryCodeName}`. -* Media library permissions are currently not supported in Xperience by Kentico and are not migrated. +- In Xperience by Kentico, Media libraries are global instead of site-specific. +- The code name of each media library on the target instance is `{SiteName}_{LibraryCodeName}`. +- Media library permissions are currently not supported in Xperience by Kentico and are not migrated. ### Attachments -* Page attachments are migrated into a media library named: *"Attachments for site \"* -* The media library contains folders matching the content tree structure for all pages with attachments (including empty folders for parent pages without attachments). The folders are named after the *node alias* of the source pages. -* Each page's folder directly contains all unsorted attachments (files added on the *Attachments* tab in the source's *Pages* application). -* Attachments stored in specific page fields are placed into subfolders, named in format: *"__fieldname"*. These subfolders can include multiple files for fields of the *Attachments* type, or a single file for *File* type fields. -* Any "floating" attachments without an associated page are migrated into the media library root folder. -* The migration does not include temporary attachments (created when a file upload is not finished correctly). If any are present on the source instance, a warning is logged in the [migration protocol](./MIGRATION_PROTOCOL_REFERENCE.md). +- Page attachments are migrated into a media library named: _"Attachments for site \"_ +- The media library contains folders matching the content tree structure for all pages with attachments (including empty folders for parent pages without attachments). The folders are named after the _node alias_ of the source pages. +- Each page's folder directly contains all unsorted attachments (files added on the _Attachments_ tab in the source's _Pages_ application). +- Attachments stored in specific page fields are placed into subfolders, named in format: _"\_\_fieldname"_. These subfolders can include multiple files for fields of the _Attachments_ type, or a single file for _File_ type fields. +- Any "floating" attachments without an associated page are migrated into the media library root folder. +- The migration does not include temporary attachments (created when a file upload is not finished correctly). If any are present on the source instance, a warning is logged in the [migration protocol](./MIGRATION_PROTOCOL_REFERENCE.md). The following is an example of a media library created by the Kentico Migration Tool for page attachments: -* **Articles** (empty parent folder) - * **Coffee-processing-techniques** (contains any unsorted attachments of the '/Articles/Coffee-processing-techniques' page) - * **__Teaser** (contains attachments stored in the page's 'Teaser' field) - * **Which-brewing-fits-you** - * **__Teaser** - * ... +- **Articles** (empty parent folder) + - **Coffee-processing-techniques** (contains any unsorted attachments of the '/Articles/Coffee-processing-techniques' page) + - **\_\_Teaser** (contains attachments stored in the page's 'Teaser' field) + - **Which-brewing-fits-you** + - **\_\_Teaser** + - ... Additionally, any attachments placed into the content of migrated pages **will no longer work** in Xperience by Kentico. This includes images and file download links that use **/getattachment** and **/getimage** URLs. -If you wish to continue using these legacy attachment URLs from earlier Kentico versions, you need to add a custom +If you wish to continue using these legacy attachment URLs from earlier product versions, you need to add a custom handler to your Xperience by Kentico project. See [`Migration.Toolkit.KXP.Extensions/README.MD`](/Migration.Toolkit.KXP.Extensions/README.MD) for instructions. diff --git a/Migration.Tool.CLI/appsettings.json b/Migration.Tool.CLI/appsettings.json index a7f377ef..249a30eb 100644 --- a/Migration.Tool.CLI/appsettings.json +++ b/Migration.Tool.CLI/appsettings.json @@ -19,8 +19,8 @@ "MigrationProtocolPath": "C:\\Logs\\protocol.txt", "KxConnectionString": "[TODO]", "KxCmsDirPath": "[TODO]", - "XbKDirPath": "[TODO]", - "XbKApiSettings": { + "XbyKDirPath": "[TODO]", + "XbyKApiSettings": { "ConnectionStrings": { "CMSConnectionString": "[TODO]" } @@ -28,6 +28,7 @@ "MigrateOnlyMediaFileInfo": false, "MigrateMediaToMediaLibrary": false, "UseDeprecatedFolderPageType": false, + "ConvertClassesToContentHub": "", "CreateReusableFieldSchemaForClasses": "", "OptInFeatures": { "QuerySourceInstanceApi": { @@ -41,11 +42,10 @@ } }, "UseOmActivityNodeRelationAutofix": "AttemptFix", - "UseOmActivitySiteRelationAutofix": "AttemptFix", + "UseOmActivitySiteRelationAutofix": "AttemptFix", "EntityConfigurations": { "CMS_Class": { - "ExcludeCodeNames": [ - "dd" + "ExcludeCodeNames": [ ] }, "CMS_SettingsKey": { diff --git a/Migration.Tool.Common/Abstractions/DefaultCustomTableClassMappingHandler.cs b/Migration.Tool.Common/Abstractions/DefaultCustomTableClassMappingHandler.cs new file mode 100644 index 00000000..9bad2b59 --- /dev/null +++ b/Migration.Tool.Common/Abstractions/DefaultCustomTableClassMappingHandler.cs @@ -0,0 +1,135 @@ +using CMS.ContentEngine; +using CMS.ContentEngine.Internal; +using CMS.Core; +using CMS.Core.Internal; +using Kentico.Xperience.UMT.Model; +using Microsoft.Extensions.Logging; +using Migration.Tool.Common.Builders; +using Migration.Tool.Common.Helpers; + +namespace Migration.Tool.Common.Abstractions; + +public class DefaultCustomTableClassMappingHandler(ILogger logger) : IClassMappingHandler +{ + private readonly IContentItemCodeNameProvider contentItemCodeNameProvider = Service.Resolve(); + + public virtual void EnsureContentItem(ContentItemModel contentItemModel, CustomTableMappingHandlerContext context) + { + if (context.Values.TryGetValue("ItemGUID", out object? mbItemGuid) && mbItemGuid is Guid itemGuid) + { + contentItemModel.ContentItemGUID = itemGuid; + logger.LogTrace("ContentItemGUID '{ContentItemGuid}' assigned from ItemGUID", contentItemModel.ContentItemGUID); + } + if (context.Values.TryGetValue("ItemID", out object? mbItemId) && mbItemId is int itemId) + { + if (contentItemModel.ContentItemGUID is null) + { + contentItemModel.ContentItemGUID = GuidHelper.CreateContentItemGuid($"{context.SourceClassName}|{itemId}"); + logger.LogTrace("ContentItemGUID '{ContentItemGuid}' derived from ItemID {ItemId}", contentItemModel.ContentItemGUID, itemId); + } + + if (contentItemModel.ContentItemName is null) + { + // intl todo - async + contentItemModel.ContentItemName = contentItemCodeNameProvider.Get($"{context.SourceClassName} - {itemId}").GetAwaiter().GetResult(); + logger.LogTrace("ContentItemName '{ContentItemName}' derived from ItemID", contentItemModel.ContentItemName); + } + } + else + { + // should lead to exception + } + } + + public virtual void EnsureContentItemCommonData(ContentItemCommonDataModel commonDataModel, CustomTableMappingHandlerContext context) + { + string guidKey = $"{context.SourceClassName}|{commonDataModel.ContentItemCommonDataContentItemGuid}|{commonDataModel.ContentItemCommonDataContentLanguageGuid}"; + commonDataModel.ContentItemCommonDataGUID = GuidHelper.CreateContentItemCommonDataGuid(guidKey); + logger.LogTrace("ContentItemCommonDataGUID '{ContentItemCommonDataGUID}' derived from key '{Key}'", commonDataModel.ContentItemCommonDataGUID, guidKey); + + commonDataModel.ContentItemCommonDataIsLatest = true; + } + + public virtual void EnsureContentItemLanguageMetadata(ContentItemLanguageMetadataModel languageMetadataInfo, CustomTableMappingHandlerContext context) + { + string guidKey = $"{context.SourceClassName}|{languageMetadataInfo.ContentItemLanguageMetadataContentItemGuid}|{languageMetadataInfo.ContentItemLanguageMetadataContentLanguageGuid}"; + languageMetadataInfo.ContentItemLanguageMetadataGUID = GuidHelper.CreateContentItemLanguageMetadataGuid(guidKey); + logger.LogTrace("ContentItemLanguageMetadataGUID '{ContentItemCommonDataGUID}' derived from key '{Key}'", languageMetadataInfo.ContentItemLanguageMetadataGUID, guidKey); + + if (languageMetadataInfo.ContentItemLanguageMetadataDisplayName is null) + { + if (context.Values.TryGetValue("ItemID", out object? mbItemId) && mbItemId is int itemId) + { + languageMetadataInfo.ContentItemLanguageMetadataDisplayName = $"{context.SourceClassName} - {itemId}"; + logger.LogTrace("ContentItemLanguageMetadataDisplayName '{ContentItemLanguageMetadataDisplayName}' derived from ItemID", languageMetadataInfo.ContentItemLanguageMetadataDisplayName); + } + } + + if (languageMetadataInfo.ContentItemLanguageMetadataCreatedWhen is null) + { + if (context.Values.TryGetValue("ItemCreatedWhen", out object? mbDate) && mbDate is DateTime date) + { + languageMetadataInfo.ContentItemLanguageMetadataCreatedWhen = date; + logger.LogTrace("ContentItemLanguageMetadataCreatedWhen '{ContentItemLanguageMetadataCreatedWhen}' derived from 'ItemCreatedWhen'", languageMetadataInfo.ContentItemLanguageMetadataCreatedWhen); + } + else + { + languageMetadataInfo.ContentItemLanguageMetadataCreatedWhen = Service.Resolve().GetDateTimeNow(); + logger.LogTrace("ContentItemLanguageMetadataCreatedWhen '{ContentItemLanguageMetadataCreatedWhen}' set to now date", languageMetadataInfo.ContentItemLanguageMetadataCreatedWhen); + } + } + + if (languageMetadataInfo.ContentItemLanguageMetadataModifiedWhen is null) + { + if (context.Values.TryGetValue("ItemModifiedWhen", out object? mbDate) && mbDate is DateTime date) + { + languageMetadataInfo.ContentItemLanguageMetadataModifiedWhen = date; + logger.LogTrace("ContentItemLanguageMetadataModifiedWhen '{ContentItemLanguageMetadataModifiedWhen}' derived from 'ItemModifiedWhen'", languageMetadataInfo.ContentItemLanguageMetadataModifiedWhen); + } + else + { + languageMetadataInfo.ContentItemLanguageMetadataModifiedWhen = Service.Resolve().GetDateTimeNow(); + logger.LogTrace("ContentItemLanguageMetadataModifiedWhen '{ContentItemLanguageMetadataModifiedWhen}' set to now date", languageMetadataInfo.ContentItemLanguageMetadataModifiedWhen); + } + } + } + + public virtual async Task> ProduceLanguageVersions(CustomTableMappingHandlerContext context) + { + var defaultContentLanguage = await Service.Resolve().GetDefaultContentLanguage(); + return + [ + new CustomTableDataLanguageVersion(defaultContentLanguage, context.Values) + ]; + + // or distribute to desired languages with own selection: + // return ContentLanguageInfo.Provider.Get().AsEnumerable().Select(cl => + // { + // return new CustomTableDataLanguageVersion(cl, context.Values); + // }).ToList(); + } + + public virtual int? GetCreatedByUserId(CustomTableMappingHandlerContext context, string sourceClassName, string sourceClassFormDefinition) + { + if (context.Values.TryGetValue("ItemCreatedByUserID", out object? mbUserId) && mbUserId is int userId) + { + return userId; + } + else + { + return null; + } + } + + public virtual int? GetModifiedByUserId(CustomTableMappingHandlerContext context, string sourceClassName, string sourceClassFormDefinition) + { + if (context.Values.TryGetValue("ItemModifiedByUserID", out object? mbUserId) && mbUserId is int userId) + { + return userId; + } + else + { + return null; + } + } +} diff --git a/Migration.Tool.Common/Abstractions/IClassMappingHandler.cs b/Migration.Tool.Common/Abstractions/IClassMappingHandler.cs new file mode 100644 index 00000000..6d5b4af5 --- /dev/null +++ b/Migration.Tool.Common/Abstractions/IClassMappingHandler.cs @@ -0,0 +1,113 @@ +using CMS.ContentEngine; +using Kentico.Xperience.UMT.Model; +using Microsoft.Extensions.Logging; +using Migration.Tool.Common.Builders; + +namespace Migration.Tool.Common.Abstractions; + +public interface IClassMappingHandler +{ + void EnsureContentItem(ContentItemModel contentItemModel, CustomTableMappingHandlerContext context); + void EnsureContentItemCommonData(ContentItemCommonDataModel commonDataModel, CustomTableMappingHandlerContext context); + void EnsureContentItemLanguageMetadata(ContentItemLanguageMetadataModel languageMetadataInfo, CustomTableMappingHandlerContext context); + Task> ProduceLanguageVersions(CustomTableMappingHandlerContext context); + int? GetCreatedByUserId(CustomTableMappingHandlerContext context, string sourceClassName, string sourceClassFormDefinition); + int? GetModifiedByUserId(CustomTableMappingHandlerContext context, string sourceClassName, string sourceClassFormDefinition); +} + +public record CustomTableDataLanguageVersion(ContentLanguageInfo Language, Dictionary Values); + +public class ClassMappingHandlerWrapper(IClassMappingHandler impl, ILogger logger) : IClassMappingHandler +{ + private readonly string implTypeFullName = impl.GetType().FullName!; + + public void EnsureContentItem(ContentItemModel contentItemModel, CustomTableMappingHandlerContext context) + { + impl.EnsureContentItem(contentItemModel, context); + + if (string.IsNullOrWhiteSpace(contentItemModel.ContentItemName)) + { + throw new InvalidOperationException($"Implementation '{implTypeFullName}.EnsureContentItem' did not set required value ContentItemName."); + } + + if (contentItemModel.ContentItemIsSecured is null) + { + throw new InvalidOperationException($"Implementation '{implTypeFullName}.EnsureContentItem' did not set required value ContentItemIsSecured."); + } + + if (contentItemModel.ContentItemGUID is null) + { + throw new InvalidOperationException($"Implementation '{implTypeFullName}.EnsureContentItem' did not set required value ContentItemGUID. Produce value in deterministic manner so migration can be repeated or create new guid, but then migration is required to be run on clean target instance everytime."); + } + } + + public void EnsureContentItemLanguageMetadata(ContentItemLanguageMetadataModel languageMetadataInfo, CustomTableMappingHandlerContext context) + { + impl.EnsureContentItemLanguageMetadata(languageMetadataInfo, context); + + if (languageMetadataInfo.ContentItemLanguageMetadataGUID is null) + { + throw new InvalidOperationException($"Implementation '{implTypeFullName}.EnsureContentItemLanguageMetadata' did not set required value ContentItemLanguageMetadataGUID. Produce value in deterministic manner so migration can be repeated or create new guid, but then migration is required to be run on clean target instance everytime."); + } + + if (languageMetadataInfo.ContentItemLanguageMetadataDisplayName is null) + { + throw new InvalidOperationException($"Implementation '{implTypeFullName}.EnsureContentItemLanguageMetadata' did not set required value ContentItemLanguageMetadataDisplayName"); + } + + if (languageMetadataInfo.ContentItemLanguageMetadataCreatedWhen is null) + { + throw new InvalidOperationException($"Implementation '{implTypeFullName}.EnsureContentItemLanguageMetadata' did not set required value ContentItemLanguageMetadataCreatedWhen"); + } + + if (languageMetadataInfo.ContentItemLanguageMetadataModifiedWhen is null) + { + throw new InvalidOperationException($"Implementation '{implTypeFullName}.EnsureContentItemLanguageMetadata' did not set required value ContentItemLanguageMetadataModifiedWhen"); + } + } + + public void EnsureContentItemCommonData(ContentItemCommonDataModel commonDataModel, CustomTableMappingHandlerContext context) + { + impl.EnsureContentItemCommonData(commonDataModel, context); + + if (commonDataModel.ContentItemCommonDataGUID is null) + { + throw new InvalidOperationException($"Implementation '{implTypeFullName}.EnsureContentItemCommonData' did not set required value ContentItemCommonDataGUID. Produce value in deterministic manner so migration can be repeated or create new guid, but then migration is required to be run on clean target instance everytime."); + } + } + + public async Task> ProduceLanguageVersions(CustomTableMappingHandlerContext context) + { + switch (await impl.ProduceLanguageVersions(context)) + { + case { Count: 0 }: + case null: + { + throw new InvalidOperationException($"Implementation '{implTypeFullName}.ProduceLanguageVersions' produced no language version, please fix custom implementation"); + } + case { Count: > 0 } languageVersions: + { + var result = await impl.ProduceLanguageVersions(context); + logger.LogTrace("{ImplTypeName}.ProduceLanguageVersions '{UserId}'", implTypeFullName, string.Join(",", languageVersions.Select(x => x.Language))); + return result; + } + + default: + break; + } + } + + public int? GetCreatedByUserId(CustomTableMappingHandlerContext context, string sourceClassName, string sourceClassFormDefinition) + { + int? userId = impl.GetCreatedByUserId(context, sourceClassName, sourceClassFormDefinition); + logger.LogTrace("{ImplTypeName}.GetCreatedByUserID '{UserId}'", implTypeFullName, userId); + return userId; + } + + public int? GetModifiedByUserId(CustomTableMappingHandlerContext context, string sourceClassName, string sourceClassFormDefinition) + { + int? userId = impl.GetModifiedByUserId(context, sourceClassName, sourceClassFormDefinition); + logger.LogTrace("{ImplTypeName}.GetModifiedByUserID '{UserId}'", implTypeFullName, userId); + return userId; + } +} diff --git a/Migration.Tool.Common/Builders/ClassMapper.cs b/Migration.Tool.Common/Builders/ClassMapper.cs index 818d618c..5720c2a4 100644 --- a/Migration.Tool.Common/Builders/ClassMapper.cs +++ b/Migration.Tool.Common/Builders/ClassMapper.cs @@ -1,5 +1,6 @@ using CMS.DataEngine; using CMS.FormEngine; +using Migration.Tool.Common.Abstractions; namespace Migration.Tool.Common.Builders; @@ -17,9 +18,14 @@ public interface IClassMapping string? GetTargetFieldName(string sourceColumnName, string sourceClassName); string GetSourceFieldName(string targetColumnName, string nodeClassClassName); - + bool IsCategoryMapped(string sourceClassName, int categoryID); void UseResusableSchema(string reusableSchemaName); IList ReusableSchemaNames { get; } + + /// + /// as for now, supported only for custom tables + /// + Type? MappingHandler { get; } } public interface IFieldMapping @@ -32,7 +38,7 @@ public interface IFieldMapping public record FieldMapping(string TargetFieldName, string SourceClassName, string SourceFieldName, bool IsTemplate) : IFieldMapping; -public record FieldMappingWithConversion(string TargetFieldName, string SourceClassName, string SourceFieldName, bool IsTemplate, Func Converter) : IFieldMapping; +public record FieldMappingWithConversion(string TargetFieldName, string SourceClassName, string SourceFieldName, bool IsTemplate, Func Converter) : IFieldMapping; public class MultiClassMapping(string targetClassName, Action classPatcher) : IClassMapping { @@ -89,10 +95,32 @@ public void UseResusableSchema(string reusableSchemaName) reusableSchemaNames.Add(reusableSchemaName); } + private MultiClassMappingCategoryFilter categoryFilter = (_, _) => true; + public void FilterCategories(MultiClassMappingCategoryFilter filter) => categoryFilter = filter; + public bool IsCategoryMapped(string sourceClassName, int categoryID) => categoryFilter(sourceClassName, categoryID); + private readonly IList reusableSchemaNames = []; IList IClassMapping.ReusableSchemaNames => reusableSchemaNames; + + #region Handlers + + public Type? MappingHandler { get; private set; } + + public void SetHandler() where T : IClassMappingHandler => MappingHandler = typeof(T); + + #endregion } +public delegate bool MultiClassMappingCategoryFilter(string sourceClassName, int categoryID); + +public interface IConvertorContext; +public record ConvertorTreeNodeContext(Guid NodeGuid, int NodeSiteId, int? DocumentId, bool MigratingFromVersionHistory) : IConvertorContext; +public record ConvertorCustomTableContext() : IConvertorContext; + +public interface IMappingHandlerContext; + +public record CustomTableMappingHandlerContext(Dictionary Values, DataClassInfo TargetClassInfo, string SourceClassName); + public class FieldBuilder(MultiClassMapping multiClassMapping, string targetFieldName) { private IFieldMapping? currentFieldMapping; @@ -105,7 +133,7 @@ public FieldBuilder SetFrom(string sourceClassName, string sourceFieldName, bool return this; } - public FieldBuilder ConvertFrom(string sourceClassName, string sourceFieldName, bool isTemplate, Func converter) + public FieldBuilder ConvertFrom(string sourceClassName, string sourceFieldName, bool isTemplate, Func converter) { currentFieldMapping = new FieldMappingWithConversion(targetFieldName, sourceClassName, sourceFieldName, isTemplate, converter); multiClassMapping.Mappings.Add(currentFieldMapping); diff --git a/Migration.Tool.Common/Commands.cs b/Migration.Tool.Common/Commands.cs index 1393020e..e50f9e99 100644 --- a/Migration.Tool.Common/Commands.cs +++ b/Migration.Tool.Common/Commands.cs @@ -136,7 +136,7 @@ public record MigrateCustomModulesCommand : IRequest, ICommand public record MigrateCustomTablesCommand : IRequest, ICommand { - public static readonly int Rank = 1 + MigrateSitesCommand.Rank; + public static readonly int Rank = 1 + MigrateSitesCommand.Rank + MigrateCustomModulesCommand.Rank; public static string Moniker => "custom-tables"; public static string MonikerFriendly => "Custom tables"; diff --git a/Migration.Tool.Common/CommonDiExtensions.cs b/Migration.Tool.Common/CommonDiExtensions.cs index 44ab123a..5daa1857 100644 --- a/Migration.Tool.Common/CommonDiExtensions.cs +++ b/Migration.Tool.Common/CommonDiExtensions.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.DependencyInjection; - +using Migration.Tool.Common.Abstractions; using Migration.Tool.Common.MigrationProtocol; namespace Migration.Tool.Common; @@ -12,6 +12,9 @@ public static IServiceCollection UseToolCommon(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + + services.AddTransient(); + return services; } } diff --git a/Migration.Tool.Common/ConfigurationNames.cs b/Migration.Tool.Common/ConfigurationNames.cs index 11e67997..60664870 100644 --- a/Migration.Tool.Common/ConfigurationNames.cs +++ b/Migration.Tool.Common/ConfigurationNames.cs @@ -5,10 +5,13 @@ public class ConfigurationNames public const string KxConnectionString = "KxConnectionString"; public const string KxCmsDirPath = "KxCmsDirPath"; - [Obsolete("not needed anymore, connection string from Kentico config section is used")] + [Obsolete("Not needed anymore, replaced by Settings:XbyKApiSettings:ConnectionStrings:CMSConnectionString")] public const string XbKConnectionString = "XbKConnectionString"; + [Obsolete("Replaced by XbyKDirPath")] public const string XbKDirPath = "XbKDirPath"; + public const string XbyKDirPath = "XbyKDirPath"; + public const string MigrateOnlyMediaFileInfo = "MigrateOnlyMediaFileInfo"; public const string UseOmActivityNodeRelationAutofix = "UseOmActivityNodeRelationAutofix"; public const string UseOmActivitySiteRelationAutofix = "UseOmActivitySiteRelationAutofix"; @@ -55,6 +58,7 @@ public class ConfigurationNames public const string FieldNameRegex = "FieldNameRegex"; public const string XbKApiSettings = "XbKApiSettings"; + public const string XbyKApiSettings = "XbyKApiSettings"; #endregion } diff --git a/Migration.Tool.Common/Enumerations/XbKSystemClasses.cs b/Migration.Tool.Common/Enumerations/XbyKSystemClasses.cs similarity index 98% rename from Migration.Tool.Common/Enumerations/XbKSystemClasses.cs rename to Migration.Tool.Common/Enumerations/XbyKSystemClasses.cs index b6c0b669..c7886de6 100644 --- a/Migration.Tool.Common/Enumerations/XbKSystemClasses.cs +++ b/Migration.Tool.Common/Enumerations/XbyKSystemClasses.cs @@ -2,7 +2,7 @@ namespace Migration.Tool.Common.Enumerations; #pragma warning disable IDE1006 -public class XbKSystemClasses +public class XbyKSystemClasses { public const string cms_culture = "cms.culture"; public const string cms_site = "cms.site"; diff --git a/Migration.Tool.Common/Enumerations/XbkSystemResource.cs b/Migration.Tool.Common/Enumerations/XbyKSystemResource.cs similarity index 97% rename from Migration.Tool.Common/Enumerations/XbkSystemResource.cs rename to Migration.Tool.Common/Enumerations/XbyKSystemResource.cs index 91f8e1f7..9342fca9 100644 --- a/Migration.Tool.Common/Enumerations/XbkSystemResource.cs +++ b/Migration.Tool.Common/Enumerations/XbyKSystemResource.cs @@ -2,7 +2,7 @@ namespace Migration.Tool.Common.Enumerations; -public static class XbkSystemResource +public static class XbyKSystemResource { public const string CMS = "CMS"; public const string CMS_ABTest = "CMS.ABTest"; diff --git a/Migration.Tool.Common/Extensions.cs b/Migration.Tool.Common/Extensions.cs index 0d24d209..a219d165 100644 --- a/Migration.Tool.Common/Extensions.cs +++ b/Migration.Tool.Common/Extensions.cs @@ -145,6 +145,7 @@ public static TEnum AsEnum(this string? value) where TEnum : struct, Enum public static int? NullIfZero(this int value) => value == 0 ? null : value; public static string? NullIf(this string? s, string value) => s == value ? null : s; + public static string? NullIf(this string? s, string value, StringComparison comparer) => string.Equals(s, value, comparer) ? null : s; public static void SetValueAsJson(this ISimpleDataContainer container, string column, TValue value) => container.SetValue(column, !value?.Equals(default) ?? false ? JsonConvert.SerializeObject(value) : null); diff --git a/Migration.Tool.Common/Helpers/GuidHelper.cs b/Migration.Tool.Common/Helpers/GuidHelper.cs index 4610630f..36971d7b 100644 --- a/Migration.Tool.Common/Helpers/GuidHelper.cs +++ b/Migration.Tool.Common/Helpers/GuidHelper.cs @@ -7,20 +7,27 @@ public static class GuidHelper public static readonly Guid GuidNsDocument = new("DCBADED0-54FC-4EEC-BB50-D6E7110E499D"); public static readonly Guid GuidNsNode = new("8691FEE4-FFFF-4642-8605-1B20B9D05360"); public static readonly Guid GuidNsTaxonomy = new("7F23EF23-F9AE-4DB8-914B-96964E6E78E6"); - public static readonly Guid GuidNsDocumentNameField = new("8935FCE5-1BDC-4677-A4CA-6DFD32F65A0F"); + public static readonly Guid GuidNsField = new("8935FCE5-1BDC-4677-A4CA-6DFD32F65A0F"); public static readonly Guid GuidNsAsset = new("9CC6DE90-8993-42D8-B4C1-1429B2F780A2"); public static readonly Guid GuidNsFolder = new("E21255AC-70F3-4A95-881A-E4AD908AF27C"); public static readonly Guid GuidNsDataClass = new("E21255AC-70F3-4A95-881A-E4AD908AF27C"); + public static readonly Guid GuidNsContentItem = new("EEBBD8D5-BA56-492F-969E-58E77EE90055"); + public static readonly Guid GuidNsContentItemCommonData = new("31BA319C-843F-482A-9841-87BC62062DC2"); + public static readonly Guid GuidNsContentItemLanguageMetadata = new("AAC0C3A9-3DE7-436E-AFAB-49C1E29D5DE2"); public static Guid CreateWebPageUrlPathGuid(string hash) => GuidV5.NewNameBased(GuidNsWebPageUrlPathInfo, hash); public static Guid CreateReusableSchemaGuid(string name) => GuidV5.NewNameBased(GuidNsReusableSchema, name); public static Guid CreateDocumentGuid(string name) => GuidV5.NewNameBased(GuidNsDocument, name); public static Guid CreateNodeGuid(string name) => GuidV5.NewNameBased(GuidNsNode, name); public static Guid CreateTaxonomyGuid(string name) => GuidV5.NewNameBased(GuidNsTaxonomy, name); - public static Guid CreateDocumentNameFieldGuid(string name) => GuidV5.NewNameBased(GuidNsDocumentNameField, name); + public static Guid CreateFieldGuid(string name) => GuidV5.NewNameBased(GuidNsField, name); public static Guid CreateAssetGuid(Guid newMediaFileGuid, string contentLanguageCode) => GuidV5.NewNameBased(GuidNsAsset, $"{newMediaFileGuid}|{contentLanguageCode}"); public static Guid CreateFolderGuid(string path) => GuidV5.NewNameBased(GuidNsFolder, path); public static Guid CreateDataClassGuid(string key) => GuidV5.NewNameBased(GuidNsDataClass, key); + public static Guid CreateContentItemGuid(string key) => GuidV5.NewNameBased(GuidNsContentItem, key); + public static Guid CreateContentItemCommonDataGuid(string key) => GuidV5.NewNameBased(GuidNsContentItemCommonData, key); + public static Guid CreateContentItemLanguageMetadataGuid(string key) => GuidV5.NewNameBased(GuidNsContentItemLanguageMetadata, key); + public static readonly Guid GuidNsLibraryFallback = new("8935FCE5-1BDC-4677-A4CA-6DFD32F65A0F"); diff --git a/Migration.Tool.Common/Migration.Tool.Common.csproj b/Migration.Tool.Common/Migration.Tool.Common.csproj index 46c9cb08..8d9b8ca1 100644 --- a/Migration.Tool.Common/Migration.Tool.Common.csproj +++ b/Migration.Tool.Common/Migration.Tool.Common.csproj @@ -17,7 +17,7 @@ - + diff --git a/Migration.Tool.Common/Resources.Designer.cs b/Migration.Tool.Common/Resources.Designer.cs index 03c36cbb..b255642f 100644 --- a/Migration.Tool.Common/Resources.Designer.cs +++ b/Migration.Tool.Common/Resources.Designer.cs @@ -1,272 +1,227 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Migration.Tool.Common -{ - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Resources - { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() - { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager - { - get - { - if (object.ReferenceEquals(resourceMan, null)) - { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Migration.Tool.Common.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture - { - get - { - return resourceCulture; - } - set - { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Attachment moved to media library, media file path: ({0}/{1}). - /// - public static string Attachment_MovedToLibrary - { - get - { - return ResourceManager.GetString("Attachment_MovedToLibrary", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Grouped attachment moved to media library, media file path: ({0}/{1}). - /// - public static string AttachmentGrouped_ModevToLibrary - { - get - { - return ResourceManager.GetString("AttachmentGrouped_ModevToLibrary", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}Configuration error{1}: {2}. - /// - public static string ConfigurationError - { - get - { - return ResourceManager.GetString("ConfigurationError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}Possible fix{1}: {2}. - /// - public static string ConfigurationRecommendedFix - { - get - { - return ResourceManager.GetString("ConfigurationRecommendedFix", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Configuration value in path 'Settings.XbKApiSettings.ConnectionStrings.CMSConnectionString' is required. - /// - public static string ConfigurationValidator_GetValidationErrors_CmsConnectionString_IsRequired - { - get - { - return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_CmsConnectionString_IsRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Section 'Settings.XbKApiSettings.ConnectionStrings' is required. - /// - public static string ConfigurationValidator_GetValidationErrors_ConnectionStrings_IsRequired - { - get - { - return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_ConnectionStrings_IsRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Section 'Settings' is required. - /// - public static string ConfigurationValidator_GetValidationErrors_Settings_IsRequired - { - get - { - return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_Settings_IsRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Configuration value in path 'Settings.KxCmsDirPath' is empty, it is recommended to set source instance filesystem path. - /// - public static string ConfigurationValidator_GetValidationErrors_SourceCmsDirPath_IsRecommended - { - get - { - return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_SourceCmsDirPath_IsRecommended", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Configuration value in path 'Settings.KxConnectionString' is required. - /// - public static string ConfigurationValidator_GetValidationErrors_SourceConnectionString_IsRequired - { - get - { - return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_SourceConnectionString_IsRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Configuration value in path 'Settings.XbKDirPath' is required. - /// - public static string ConfigurationValidator_GetValidationErrors_TargetCmsDirPath_IsRequired - { - get - { - return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_TargetCmsDirPath_IsRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Configuration value in path 'Settings.XbKConnectionString' is required. - /// - public static string ConfigurationValidator_GetValidationErrors_TargetConnectionString_IsRequired - { - get - { - return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_TargetConnectionString_IsRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Section 'Settings.XbKApiSettings' is required. - /// - public static string ConfigurationValidator_GetValidationErrors_TargetKxpApiSettings_IsRequired - { - get - { - return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_TargetKxpApiSettings_IsRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Configuration value in path 'Settings.UseOmActivityNodeRelationAutofix' must fit one of: {0}. - /// - public static string ConfigurationValidator_GetValidationErrors_UseOmActivityNodeRelationAutofix_MustFit - { - get - { - return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_UseOmActivityNodeRelationAutofix_MustF" + - "it", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Configuration value in path 'Settings.UseOmActivitySiteRelationAutofix' must fit one of: {0}. - /// - public static string ConfigurationValidator_GetValidationErrors_UseOmActivitySiteRelationAutofix_MustFit - { - get - { - return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_UseOmActivitySiteRelationAutofix_MustF" + - "it", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}Configuration warning{1}: {2}. - /// - public static string ConfigurationWarning - { - get - { - return ResourceManager.GetString("ConfigurationWarning", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Document relationship is currently not supported - related to: {0}. - /// - public static string Document_Relationship_NotSupported - { - get - { - return ResourceManager.GetString("Document_Relationship_NotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} ExplicitPrimaryKeyMapping of {1} is required.. - /// - public static string Exception_MappingIsRequired - { - get - { - return ResourceManager.GetString("Exception_MappingIsRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Press any key to exit program.... - /// - public static string ProgramAwaitingExitMessage - { - get - { - return ResourceManager.GetString("ProgramAwaitingExitMessage", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Migration.Tool.Common { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Migration.Tool.Common.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Attachment moved to media library, media file path: ({0}/{1}). + /// + public static string Attachment_MovedToLibrary { + get { + return ResourceManager.GetString("Attachment_MovedToLibrary", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Grouped attachment moved to media library, media file path: ({0}/{1}). + /// + public static string AttachmentGrouped_ModevToLibrary { + get { + return ResourceManager.GetString("AttachmentGrouped_ModevToLibrary", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}Configuration error{1}: {2}. + /// + public static string ConfigurationError { + get { + return ResourceManager.GetString("ConfigurationError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}Possible fix{1}: {2}. + /// + public static string ConfigurationRecommendedFix { + get { + return ResourceManager.GetString("ConfigurationRecommendedFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuration value in path 'Settings.XbyKApiSettings.ConnectionStrings.CMSConnectionString' is required. + /// + public static string ConfigurationValidator_GetValidationErrors_CmsConnectionString_IsRequired { + get { + return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_CmsConnectionString_IsRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Section 'Settings.XbyKApiSettings.ConnectionStrings' is required. + /// + public static string ConfigurationValidator_GetValidationErrors_ConnectionStrings_IsRequired { + get { + return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_ConnectionStrings_IsRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Section 'Settings' is required. + /// + public static string ConfigurationValidator_GetValidationErrors_Settings_IsRequired { + get { + return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_Settings_IsRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuration value in path 'Settings.KxCmsDirPath' is empty, it is recommended to set source instance filesystem path. + /// + public static string ConfigurationValidator_GetValidationErrors_SourceCmsDirPath_IsRecommended { + get { + return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_SourceCmsDirPath_IsRecommended", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuration value in path 'Settings.KxConnectionString' is required. + /// + public static string ConfigurationValidator_GetValidationErrors_SourceConnectionString_IsRequired { + get { + return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_SourceConnectionString_IsRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuration value in path 'Settings.XbyKDirPath' is required. + /// + public static string ConfigurationValidator_GetValidationErrors_TargetCmsDirPath_IsRequired { + get { + return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_TargetCmsDirPath_IsRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuration value in path 'Settings.XbKConnectionString' is required. + /// + public static string ConfigurationValidator_GetValidationErrors_TargetConnectionString_IsRequired { + get { + return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_TargetConnectionString_IsRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Section 'Settings.XbyKApiSettings' is required. + /// + public static string ConfigurationValidator_GetValidationErrors_TargetKxpApiSettings_IsRequired { + get { + return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_TargetKxpApiSettings_IsRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuration value in path 'Settings.UseOmActivityNodeRelationAutofix' must fit one of: {0}. + /// + public static string ConfigurationValidator_GetValidationErrors_UseOmActivityNodeRelationAutofix_MustFit { + get { + return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_UseOmActivityNodeRelationAutofix_MustF" + + "it", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuration value in path 'Settings.UseOmActivitySiteRelationAutofix' must fit one of: {0}. + /// + public static string ConfigurationValidator_GetValidationErrors_UseOmActivitySiteRelationAutofix_MustFit { + get { + return ResourceManager.GetString("ConfigurationValidator_GetValidationErrors_UseOmActivitySiteRelationAutofix_MustF" + + "it", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}Configuration warning{1}: {2}. + /// + public static string ConfigurationWarning { + get { + return ResourceManager.GetString("ConfigurationWarning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Document relationship is currently not supported - related to: {0}. + /// + public static string Document_Relationship_NotSupported { + get { + return ResourceManager.GetString("Document_Relationship_NotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} ExplicitPrimaryKeyMapping of {1} is required.. + /// + public static string Exception_MappingIsRequired { + get { + return ResourceManager.GetString("Exception_MappingIsRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Finished, press any key to exit program.... + /// + public static string ProgramAwaitingExitMessage { + get { + return ResourceManager.GetString("ProgramAwaitingExitMessage", resourceCulture); + } + } + } +} diff --git a/Migration.Tool.Common/Resources.resx b/Migration.Tool.Common/Resources.resx index 525afa32..f17ffb74 100644 --- a/Migration.Tool.Common/Resources.resx +++ b/Migration.Tool.Common/Resources.resx @@ -1,82 +1,82 @@ - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - Section 'Settings' is required - - - Configuration value in path 'Settings.KxConnectionString' is required - - - Configuration value in path 'Settings.KxCmsDirPath' is empty, it is recommended to set source instance filesystem path - - - Configuration value in path 'Settings.XbKConnectionString' is required - - - Configuration value in path 'Settings.XbKDirPath' is required - - - Section 'Settings.XbKApiSettings' is required - - - Section 'Settings.XbKApiSettings.ConnectionStrings' is required - - - Configuration value in path 'Settings.XbKApiSettings.ConnectionStrings.CMSConnectionString' is required - - - Configuration value in path 'Settings.UseOmActivitySiteRelationAutofix' must fit one of: {0} - - - Configuration value in path 'Settings.UseOmActivityNodeRelationAutofix' must fit one of: {0} - - - {0}Configuration error{1}: {2} - - - {0}Possible fix{1}: {2} - - - {0}Configuration warning{1}: {2} - - - Press any key to exit program... - - - {0} ExplicitPrimaryKeyMapping of {1} is required. - - - Attachment moved to media library, media file path: ({0}/{1}) - - - Grouped attachment moved to media library, media file path: ({0}/{1}) - - - Document relationship is currently not supported - related to: {0} - + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + Section 'Settings' is required + + + Configuration value in path 'Settings.KxConnectionString' is required + + + Configuration value in path 'Settings.KxCmsDirPath' is empty, it is recommended to set source instance filesystem path + + + Configuration value in path 'Settings.XbKConnectionString' is required + + + Configuration value in path 'Settings.XbyKDirPath' is required + + + Section 'Settings.XbyKApiSettings' is required + + + Section 'Settings.XbyKApiSettings.ConnectionStrings' is required + + + Configuration value in path 'Settings.XbyKApiSettings.ConnectionStrings.CMSConnectionString' is required + + + Configuration value in path 'Settings.UseOmActivitySiteRelationAutofix' must fit one of: {0} + + + Configuration value in path 'Settings.UseOmActivityNodeRelationAutofix' must fit one of: {0} + + + {0}Configuration error{1}: {2} + + + {0}Possible fix{1}: {2} + + + {0}Configuration warning{1}: {2} + + + Finished, press any key to exit program... + + + {0} ExplicitPrimaryKeyMapping of {1} is required. + + + Attachment moved to media library, media file path: ({0}/{1}) + + + Grouped attachment moved to media library, media file path: ({0}/{1}) + + + Document relationship is currently not supported - related to: {0} + \ No newline at end of file diff --git a/Migration.Tool.Common/ToolConfiguration.cs b/Migration.Tool.Common/ToolConfiguration.cs index 15249bb6..dbe4dcd7 100644 --- a/Migration.Tool.Common/ToolConfiguration.cs +++ b/Migration.Tool.Common/ToolConfiguration.cs @@ -53,12 +53,12 @@ public class ToolConfiguration public string? ConvertClassesToContentHub { get; set; } public IReadOnlySet ClassNamesCreateReusableSchema => classNamesCreateReusableSchema ??= new HashSet( - (CreateReusableFieldSchemaForClasses?.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) ?? []).Select(x => x.Trim()), + (CreateReusableFieldSchemaForClasses?.Split(new[] { ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries) ?? []).Select(x => x.Trim()), StringComparer.InvariantCultureIgnoreCase ); public IReadOnlySet ClassNamesConvertToContentHub => classNamesConvertToContentHub ??= new HashSet( - (ConvertClassesToContentHub?.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) ?? []).Select(x => x.Trim()), + (ConvertClassesToContentHub?.Split(new[] { ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries) ?? []).Select(x => x.Trim()), StringComparer.InvariantCultureIgnoreCase ); @@ -87,15 +87,15 @@ public string KxConnectionString [ConfigurationKeyName(ConfigurationNames.XbKConnectionString)] public string XbKConnectionString { - get => xbKConnectionString!; - set => xbKConnectionString = value; + get => xbkConnectionString!; + set => xbkConnectionString = value; } public void SetXbKConnectionStringIfNotEmpty(string? connectionString) { if (!string.IsNullOrWhiteSpace(connectionString)) { - xbKConnectionString = connectionString; + xbkConnectionString = connectionString; } } @@ -105,11 +105,14 @@ public void SetXbKConnectionStringIfNotEmpty(string? connectionString) private HashSet? classNamesConvertToContentHub; private HashSet? classNamesCreateReusableSchema; - private string? xbKConnectionString; + private string? xbkConnectionString; [ConfigurationKeyName(ConfigurationNames.XbKDirPath)] public string? XbKDirPath { get; set; } = null; + [ConfigurationKeyName(ConfigurationNames.XbyKDirPath)] + public string? XbyKDirPath { get; set; } = null; + #endregion [ConfigurationKeyName(ConfigurationNames.UrlProtocol)] diff --git a/Migration.Tool.Core.K11/Providers/ContentItemNameProvider.cs b/Migration.Tool.Core.K11/Providers/ContentItemNameProvider.cs deleted file mode 100644 index 54164104..00000000 --- a/Migration.Tool.Core.K11/Providers/ContentItemNameProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -using CMS.Base; -using CMS.ContentEngine.Internal; -using CMS.Helpers; - -namespace Migration.Tool.Core.K11.Providers; - -internal class ContentItemNameProvider -{ - private readonly IContentItemNameValidator codeNameValidator; - - - /// - /// Creates a new instance of . - /// - public ContentItemNameProvider(IContentItemNameValidator codeNameValidator) => this.codeNameValidator = codeNameValidator; - - public Task Get(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name)); - } - - async Task Get(string name) - { - string codeName = ValidationHelper.GetCodeName(name, useUnicode: false); - - bool isCodeNameValid = ValidationHelper.IsCodeName(codeName); - - if (string.IsNullOrEmpty(codeName) || !isCodeNameValid) - { - codeName = TypeHelper.GetNiceName(ContentItemInfo.OBJECT_TYPE); - } - - var uniqueCodeNameProvider = new UniqueContentItemNameProvider(codeNameValidator); - - return await uniqueCodeNameProvider.GetUniqueValue(codeName); - } - - return Get(name); - } -} diff --git a/Migration.Tool.Core.K11/Providers/ContentItemNameValidator.cs b/Migration.Tool.Core.K11/Providers/ContentItemNameValidator.cs deleted file mode 100644 index 8aa18913..00000000 --- a/Migration.Tool.Core.K11/Providers/ContentItemNameValidator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using CMS.ContentEngine.Internal; - -namespace Migration.Tool.Core.K11.Providers; - -internal class ContentItemNameValidator : IContentItemNameValidator -{ - /// - public bool IsUnique(string name) => IsUnique(0, name); - - - /// - public bool IsUnique(int id, string name) - { - var contentItemInfo = new ContentItemInfo { ContentItemID = id, ContentItemName = name }; - - return contentItemInfo.CheckUniqueCodeName(); - } -} diff --git a/Migration.Tool.Core.K11/Providers/UniqueContentItemNameProvider.cs b/Migration.Tool.Core.K11/Providers/UniqueContentItemNameProvider.cs deleted file mode 100644 index b0476fc7..00000000 --- a/Migration.Tool.Core.K11/Providers/UniqueContentItemNameProvider.cs +++ /dev/null @@ -1,38 +0,0 @@ -using CMS.Base; -using CMS.ContentEngine.Internal; - -namespace Migration.Tool.Core.K11.Providers; - -internal class UniqueContentItemNameProvider : UniqueStringValueProviderBase -{ - private readonly IContentItemNameValidator codeNameValidator; - - - /// - /// Creates a new instance of . - /// - public UniqueContentItemNameProvider(IContentItemNameValidator codeNameValidator) - : base(TypeHelper.GetMaxCodeNameLength(ContentItemInfo.TYPEINFO.MaxCodeNameLength)) => this.codeNameValidator = codeNameValidator; - - public override Task GetUniqueValue(string inputValue) => base.GetUniqueValue(AddSuffix(inputValue)); - - - private string AddSuffix(string codeName) - { - string randomSuffix = GetRandomSuffix(); - string codeNameWithSuffix = codeName += randomSuffix; - - if (codeNameWithSuffix.Length > MaxLength) - { - int availableLength = MaxLength - randomSuffix.Length; - - codeNameWithSuffix = $"{codeName[..availableLength]}{randomSuffix}"; - } - - return codeNameWithSuffix; - } - - - /// - protected override Task IsValueUnique(string value) => Task.FromResult(codeNameValidator.IsUnique(value)); -} diff --git a/Migration.Tool.Core.K11/Services/CountryMigrator.cs b/Migration.Tool.Core.K11/Services/CountryMigrator.cs index 9bdd4916..105d1249 100644 --- a/Migration.Tool.Core.K11/Services/CountryMigrator.cs +++ b/Migration.Tool.Core.K11/Services/CountryMigrator.cs @@ -26,7 +26,7 @@ public void MigrateCountriesAndStates() { if (!kxpApiInitializer.EnsureApiIsInitialized()) { - throw new InvalidOperationException("Falied to initialize kentico API. Please check configuration."); + throw new InvalidOperationException("Failed to initialize Kentico API. Please check configuration."); } var k11Context = k11ContextFactory.CreateDbContext(); diff --git a/Migration.Tool.Core.KX12/Behaviors/CommandConstraintBehavior.cs b/Migration.Tool.Core.KX12/Behaviors/CommandConstraintBehavior.cs index ff3bb893..7cb3541b 100644 --- a/Migration.Tool.Core.KX12/Behaviors/CommandConstraintBehavior.cs +++ b/Migration.Tool.Core.KX12/Behaviors/CommandConstraintBehavior.cs @@ -75,28 +75,28 @@ private bool CheckVersion(KX12Context kx12Context, SemanticVersion minimalVersio void UnableToReadVersionKey(string keyName) { - logger.LogCritical("Unable to read CMS version (incorrect format) - SettingsKeyName '{Key}'. Ensure Kentico version is at least '{SupportedVersion}'", keyName, minimalVersion.ToString()); + logger.LogCritical("Unable to read CMS version (incorrect format) - SettingsKeyName '{Key}'. Ensure Kentico 12 version is at least '{SupportedVersion}'", keyName, minimalVersion.ToString()); protocol.Append(HandbookReferences.InvalidSourceCmsVersion().WithData(new { ErrorKind = "Settings key value incorrect format", SettingsKeyName = keyName, SupportedVersion = minimalVersion.ToString() })); criticalCheckPassed = false; } void VersionKeyNotFound(string keyName) { - logger.LogCritical("CMS version not found - SettingsKeyName '{Key}'. Ensure Kentico version is at least '{SupportedVersion}'", keyName, minimalVersion.ToString()); + logger.LogCritical("CMS version not found - SettingsKeyName '{Key}'. Ensure Kentico 12 version is at least '{SupportedVersion}'", keyName, minimalVersion.ToString()); protocol.Append(HandbookReferences.InvalidSourceCmsVersion().WithData(new { ErrorKind = "Settings key not found", SettingsKeyName = keyName, SupportedVersion = minimalVersion.ToString() })); criticalCheckPassed = false; } void UpgradeNeeded(string keyName, string currentVersion) { - logger.LogCritical("{Key} '{CurrentVersion}' is not supported for migration. Upgrade Kentico to at least '{SupportedVersion}'", keyName, currentVersion, minimalVersion.ToString()); + logger.LogCritical("{Key} '{CurrentVersion}' is not supported for migration. Upgrade Kentico 12 to at least '{SupportedVersion}'", keyName, currentVersion, minimalVersion.ToString()); protocol.Append(HandbookReferences.InvalidSourceCmsVersion().WithData(new { CurrentVersion = currentVersion, SupportedVersion = minimalVersion.ToString() })); criticalCheckPassed = false; } void LowHotfix(string keyName, int currentHotfix) { - logger.LogCritical("{Key} '{CurrentVersion}' hotfix is not supported for migration. Upgrade Kentico to at least '{SupportedVersion}'", keyName, currentHotfix, minimalVersion.ToString()); + logger.LogCritical("{Key} '{CurrentVersion}' hotfix is not supported for migration. Upgrade Kentico 12 to at least '{SupportedVersion}'", keyName, currentHotfix, minimalVersion.ToString()); protocol.Append(HandbookReferences.InvalidSourceCmsVersion().WithData(new { CurrentHotfix = currentHotfix.ToString(), SupportedVersion = minimalVersion.ToString() })); criticalCheckPassed = false; } diff --git a/Migration.Tool.Core.KX12/Providers/ContentItemNameProvider.cs b/Migration.Tool.Core.KX12/Providers/ContentItemNameProvider.cs deleted file mode 100644 index 631e5fee..00000000 --- a/Migration.Tool.Core.KX12/Providers/ContentItemNameProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -using CMS.Base; -using CMS.ContentEngine.Internal; -using CMS.Helpers; - -namespace Migration.Tool.Core.KX12.Providers; - -internal class ContentItemNameProvider -{ - private readonly IContentItemNameValidator codeNameValidator; - - - /// - /// Creates a new instance of . - /// - public ContentItemNameProvider(IContentItemNameValidator codeNameValidator) => this.codeNameValidator = codeNameValidator; - - public Task Get(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name)); - } - - async Task Get(string name) - { - string codeName = ValidationHelper.GetCodeName(name, useUnicode: false); - - bool isCodeNameValid = ValidationHelper.IsCodeName(codeName); - - if (string.IsNullOrEmpty(codeName) || !isCodeNameValid) - { - codeName = TypeHelper.GetNiceName(ContentItemInfo.OBJECT_TYPE); - } - - var uniqueCodeNameProvider = new UniqueContentItemNameProvider(codeNameValidator); - - return await uniqueCodeNameProvider.GetUniqueValue(codeName); - } - - return Get(name); - } -} diff --git a/Migration.Tool.Core.KX12/Providers/ContentItemNameValidator.cs b/Migration.Tool.Core.KX12/Providers/ContentItemNameValidator.cs deleted file mode 100644 index c1e2f1b0..00000000 --- a/Migration.Tool.Core.KX12/Providers/ContentItemNameValidator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using CMS.ContentEngine.Internal; - -namespace Migration.Tool.Core.KX12.Providers; - -internal class ContentItemNameValidator : IContentItemNameValidator -{ - /// - public bool IsUnique(string name) => IsUnique(0, name); - - - /// - public bool IsUnique(int id, string name) - { - var contentItemInfo = new ContentItemInfo { ContentItemID = id, ContentItemName = name }; - - return contentItemInfo.CheckUniqueCodeName(); - } -} diff --git a/Migration.Tool.Core.KX12/Providers/UniqueContentItemNameProvider.cs b/Migration.Tool.Core.KX12/Providers/UniqueContentItemNameProvider.cs deleted file mode 100644 index 88fcf74d..00000000 --- a/Migration.Tool.Core.KX12/Providers/UniqueContentItemNameProvider.cs +++ /dev/null @@ -1,38 +0,0 @@ -using CMS.Base; -using CMS.ContentEngine.Internal; - -namespace Migration.Tool.Core.KX12.Providers; - -internal class UniqueContentItemNameProvider : UniqueStringValueProviderBase -{ - private readonly IContentItemNameValidator codeNameValidator; - - - /// - /// Creates a new instance of . - /// - public UniqueContentItemNameProvider(IContentItemNameValidator codeNameValidator) - : base(TypeHelper.GetMaxCodeNameLength(ContentItemInfo.TYPEINFO.MaxCodeNameLength)) => this.codeNameValidator = codeNameValidator; - - public override Task GetUniqueValue(string inputValue) => base.GetUniqueValue(AddSuffix(inputValue)); - - - private string AddSuffix(string codeName) - { - string randomSuffix = GetRandomSuffix(); - string codeNameWithSuffix = codeName += randomSuffix; - - if (codeNameWithSuffix.Length > MaxLength) - { - int availableLength = MaxLength - randomSuffix.Length; - - codeNameWithSuffix = $"{codeName[..availableLength]}{randomSuffix}"; - } - - return codeNameWithSuffix; - } - - - /// - protected override Task IsValueUnique(string value) => Task.FromResult(codeNameValidator.IsUnique(value)); -} diff --git a/Migration.Tool.Core.KX12/Services/CountryMigrator.cs b/Migration.Tool.Core.KX12/Services/CountryMigrator.cs index e7062ec4..c025076d 100644 --- a/Migration.Tool.Core.KX12/Services/CountryMigrator.cs +++ b/Migration.Tool.Core.KX12/Services/CountryMigrator.cs @@ -25,7 +25,7 @@ public void MigrateCountriesAndStates() { if (!kxpApiInitializer.EnsureApiIsInitialized()) { - throw new InvalidOperationException("Falied to initialize kentico API. Please check configuration."); + throw new InvalidOperationException("Failed to initialize Kentico API. Please check configuration."); } var kx12Context = kx12ContextFactory.CreateDbContext(); diff --git a/Migration.Tool.Core.KX13/Behaviors/CommandConstraintBehavior.cs b/Migration.Tool.Core.KX13/Behaviors/CommandConstraintBehavior.cs index cccf2d98..33ea72d5 100644 --- a/Migration.Tool.Core.KX13/Behaviors/CommandConstraintBehavior.cs +++ b/Migration.Tool.Core.KX13/Behaviors/CommandConstraintBehavior.cs @@ -78,28 +78,28 @@ private bool CheckVersion(KX13Context kx13Context, SemanticVersion minimalVersio void UnableToReadVersionKey(string keyName) { - logger.LogCritical("Unable to read CMS version (incorrect format) - SettingsKeyName '{Key}'. Ensure Kentico version is at least '{SupportedVersion}'", keyName, minimalVersion.ToString()); + logger.LogCritical("Unable to read CMS version (incorrect format) - SettingsKeyName '{Key}'. Ensure Kentico Xperience 13 version is at least '{SupportedVersion}'", keyName, minimalVersion.ToString()); protocol.Append(HandbookReferences.InvalidSourceCmsVersion().WithData(new { ErrorKind = "Settings key value incorrect format", SettingsKeyName = keyName, SupportedVersion = minimalVersion.ToString() })); criticalCheckPassed = false; } void VersionKeyNotFound(string keyName) { - logger.LogCritical("CMS version not found - SettingsKeyName '{Key}'. Ensure Kentico version is at least '{SupportedVersion}'", keyName, minimalVersion.ToString()); + logger.LogCritical("CMS version not found - SettingsKeyName '{Key}'. Ensure Kentico Xperience 13 version is at least '{SupportedVersion}'", keyName, minimalVersion.ToString()); protocol.Append(HandbookReferences.InvalidSourceCmsVersion().WithData(new { ErrorKind = "Settings key not found", SettingsKeyName = keyName, SupportedVersion = minimalVersion.ToString() })); criticalCheckPassed = false; } void UpgradeNeeded(string keyName, string currentVersion) { - logger.LogCritical("{Key} '{CurrentVersion}' is not supported for migration. Upgrade Kentico to at least '{SupportedVersion}'", keyName, currentVersion, minimalVersion.ToString()); + logger.LogCritical("{Key} '{CurrentVersion}' is not supported for migration. Upgrade Kentico Xperience 13 to at least '{SupportedVersion}'", keyName, currentVersion, minimalVersion.ToString()); protocol.Append(HandbookReferences.InvalidSourceCmsVersion().WithData(new { CurrentVersion = currentVersion, SupportedVersion = minimalVersion.ToString() })); criticalCheckPassed = false; } void LowHotfix(string keyName, int currentHotfix) { - logger.LogCritical("{Key} '{CurrentVersion}' hotfix is not supported for migration. Upgrade Kentico to at least '{SupportedVersion}'", keyName, currentHotfix, minimalVersion.ToString()); + logger.LogCritical("{Key} '{CurrentVersion}' hotfix is not supported for migration. Upgrade Kentico Xperience 13 to at least '{SupportedVersion}'", keyName, currentHotfix, minimalVersion.ToString()); protocol.Append(HandbookReferences.InvalidSourceCmsVersion().WithData(new { CurrentHotfix = currentHotfix.ToString(), SupportedVersion = minimalVersion.ToString() })); criticalCheckPassed = false; } diff --git a/Migration.Tool.Core.KX13/Migration.Tool.Core.KX13.csproj b/Migration.Tool.Core.KX13/Migration.Tool.Core.KX13.csproj index 1c7ebd1e..9d040107 100644 --- a/Migration.Tool.Core.KX13/Migration.Tool.Core.KX13.csproj +++ b/Migration.Tool.Core.KX13/Migration.Tool.Core.KX13.csproj @@ -11,7 +11,7 @@ - + diff --git a/Migration.Tool.Core.KX13/Providers/ContentItemNameProvider.cs b/Migration.Tool.Core.KX13/Providers/ContentItemNameProvider.cs deleted file mode 100644 index 57b79fc7..00000000 --- a/Migration.Tool.Core.KX13/Providers/ContentItemNameProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -using CMS.Base; -using CMS.ContentEngine.Internal; -using CMS.Helpers; - -namespace Migration.Tool.Core.KX13.Providers; - -internal class ContentItemNameProvider -{ - private readonly IContentItemNameValidator codeNameValidator; - - - /// - /// Creates a new instance of . - /// - public ContentItemNameProvider(IContentItemNameValidator codeNameValidator) => this.codeNameValidator = codeNameValidator; - - public Task Get(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name)); - } - - async Task Get(string name) - { - string codeName = ValidationHelper.GetCodeName(name, useUnicode: false); - - bool isCodeNameValid = ValidationHelper.IsCodeName(codeName); - - if (string.IsNullOrEmpty(codeName) || !isCodeNameValid) - { - codeName = TypeHelper.GetNiceName(ContentItemInfo.OBJECT_TYPE); - } - - var uniqueCodeNameProvider = new UniqueContentItemNameProvider(codeNameValidator); - - return await uniqueCodeNameProvider.GetUniqueValue(codeName); - } - - return Get(name); - } -} diff --git a/Migration.Tool.Core.KX13/Providers/ContentItemNameValidator.cs b/Migration.Tool.Core.KX13/Providers/ContentItemNameValidator.cs deleted file mode 100644 index b7e07119..00000000 --- a/Migration.Tool.Core.KX13/Providers/ContentItemNameValidator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using CMS.ContentEngine.Internal; - -namespace Migration.Tool.Core.KX13.Providers; - -internal class ContentItemNameValidator : IContentItemNameValidator -{ - /// - public bool IsUnique(string name) => IsUnique(0, name); - - - /// - public bool IsUnique(int id, string name) - { - var contentItemInfo = new ContentItemInfo { ContentItemID = id, ContentItemName = name }; - - return contentItemInfo.CheckUniqueCodeName(); - } -} diff --git a/Migration.Tool.Core.KX13/Providers/UniqueContentItemNameProvider.cs b/Migration.Tool.Core.KX13/Providers/UniqueContentItemNameProvider.cs deleted file mode 100644 index 0067906d..00000000 --- a/Migration.Tool.Core.KX13/Providers/UniqueContentItemNameProvider.cs +++ /dev/null @@ -1,38 +0,0 @@ -using CMS.Base; -using CMS.ContentEngine.Internal; - -namespace Migration.Tool.Core.KX13.Providers; - -internal class UniqueContentItemNameProvider : UniqueStringValueProviderBase -{ - private readonly IContentItemNameValidator codeNameValidator; - - - /// - /// Creates a new instance of . - /// - public UniqueContentItemNameProvider(IContentItemNameValidator codeNameValidator) - : base(TypeHelper.GetMaxCodeNameLength(ContentItemInfo.TYPEINFO.MaxCodeNameLength)) => this.codeNameValidator = codeNameValidator; - - public override Task GetUniqueValue(string inputValue) => base.GetUniqueValue(AddSuffix(inputValue)); - - - private string AddSuffix(string codeName) - { - string randomSuffix = GetRandomSuffix(); - string codeNameWithSuffix = codeName += randomSuffix; - - if (codeNameWithSuffix.Length > MaxLength) - { - int availableLength = MaxLength - randomSuffix.Length; - - codeNameWithSuffix = $"{codeName[..availableLength]}{randomSuffix}"; - } - - return codeNameWithSuffix; - } - - - /// - protected override Task IsValueUnique(string value) => Task.FromResult(codeNameValidator.IsUnique(value)); -} diff --git a/Migration.Tool.Core.KX13/Services/CountryMigrator.cs b/Migration.Tool.Core.KX13/Services/CountryMigrator.cs index 8189df7e..d2988d1f 100644 --- a/Migration.Tool.Core.KX13/Services/CountryMigrator.cs +++ b/Migration.Tool.Core.KX13/Services/CountryMigrator.cs @@ -25,7 +25,7 @@ public void MigrateCountriesAndStates() { if (!kxpApiInitializer.EnsureApiIsInitialized()) { - throw new InvalidOperationException("Falied to initialize kentico API. Please check configuration."); + throw new InvalidOperationException("Failed to initialize Kentico API. Please check configuration."); } var kx13Context = kx13ContextFactory.CreateDbContext(); diff --git a/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs b/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs index fa0c0026..56f0730d 100644 --- a/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs +++ b/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs @@ -1,7 +1,11 @@ using CMS.DataEngine; using CMS.FormEngine; +using Kentico.Xperience.UMT.Model; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Migration.Tool.Common.Abstractions; using Migration.Tool.Common.Builders; +using Migration.Tool.Common.Helpers; using Migration.Tool.KXP.Api.Auxiliary; // ReSharper disable ArrangeMethodOrOperatorBody @@ -128,6 +132,7 @@ public static IServiceCollection AddSimpleRemodelingSample(this IServiceCollecti public static IServiceCollection AddClassMergeExample(this IServiceCollection serviceCollection) { const string targetClassName = "ET.Event"; + const string sourceClassName1 = "_ET.Event1"; const string sourceClassName2 = "_ET.Event2"; @@ -140,6 +145,90 @@ public static IServiceCollection AddClassMergeExample(this IServiceCollection se target.ClassContentTypeType = ClassContentTypeType.WEBSITE; }); + // register custom table handler once for all custom table mappings + serviceCollection.AddTransient(); + + m.BuildField("EventID").AsPrimaryKey(); + + // build new field + var title = m.BuildField("Title"); + // map "EventTitle" field form source data class "_ET.Event1" also use it as template for target field + title.SetFrom(sourceClassName1, "EventTitle", true); + // map "EventTitle" field form source data class "_ET.Event2" + title.SetFrom(sourceClassName2, "EventTitle"); + // patch field definition, in this case lets change field caption + title.WithFieldPatch(f => f.Caption = "Event title"); + + + var description = m.BuildField("Description"); + description.SetFrom(sourceClassName2, "EventSmallDesc", true); + description.WithFieldPatch(f => f.Caption = "Event description"); + + var teaser = m.BuildField("Teaser"); + teaser.SetFrom(sourceClassName1, "EventTeaser", true); + teaser.WithFieldPatch(f => f.Caption = "Event teaser"); + + var text = m.BuildField("Text"); + text.SetFrom(sourceClassName1, "EventText", true); + text.SetFrom(sourceClassName2, "EventHtml"); + text.WithFieldPatch(f => f.Caption = "Event text"); + + var startDate = m.BuildField("StartDate"); + startDate.SetFrom(sourceClassName1, "EventDateStart", true); + // if needed use value conversion to adapt value + Func dateConvertor = (v, context) => + { + switch (context) + { + case ConvertorTreeNodeContext treeNodeContext: + // here you can use available treenode context + // (var nodeGuid, int nodeSiteId, int? documentId, bool migratingFromVersionHistory) = treeNodeContext; + break; + default: + // no context is available (in future, mapping feature could be extended and therefore different context will be supplied or no context at all) + break; + } + + // be strict about value assertion and isolate any unexpected value + + return v switch + { + string stringValue => !string.IsNullOrWhiteSpace(stringValue) ? DateTime.Parse(stringValue) : null, // valid date or empty string isd expected, otherwise error + DateTime dateTimeValue => dateTimeValue, + null => null, + _ => throw new InvalidOperationException($"I didn't expected this value in instance of my class! {v}") + }; + }; + startDate.ConvertFrom(sourceClassName2, "EventStartDateAsText", false, dateConvertor); + startDate.WithFieldPatch(f => f.Caption = "Event start date"); + + serviceCollection.AddSingleton(m); + + return serviceCollection; + } + + public static IServiceCollection AddClassMergeExampleAsReusable(this IServiceCollection serviceCollection) + { + const string targetClassName = "ET.Event"; + + const string sourceClassName1 = "_ET.Event1"; + const string sourceClassName2 = "_ET.Event2"; + const string sourceClassName3 = "_ET.EventCustomTable"; + + var m = new MultiClassMapping(targetClassName, target => + { + target.ClassName = targetClassName; + target.ClassTableName = "ET_Event"; + target.ClassDisplayName = "ET - MY new transformed event"; + target.ClassType = ClassType.CONTENT_TYPE; + target.ClassContentTypeType = ClassContentTypeType.REUSABLE; + }); + + m.SetHandler(); + + // register custom table handler once for all custom table mappings + serviceCollection.AddTransient(); + m.BuildField("EventID").AsPrimaryKey(); // build new field @@ -148,13 +237,27 @@ public static IServiceCollection AddClassMergeExample(this IServiceCollection se title.SetFrom(sourceClassName1, "EventTitle", true); // map "EventTitle" field form source data class "_ET.Event2" title.SetFrom(sourceClassName2, "EventTitle"); + // map "TitleCT" from custom table + title.SetFrom(sourceClassName3, "TitleCT"); // patch field definition, in this case lets change field caption title.WithFieldPatch(f => f.Caption = "Event title"); + var description = m.BuildField("Description"); description.SetFrom(sourceClassName2, "EventSmallDesc", true); + description.SetFrom(sourceClassName3, "DescriptionCT"); description.WithFieldPatch(f => f.Caption = "Event description"); + var descriptionCopy = m.BuildField("DescriptionCopy"); + descriptionCopy.SetFrom(sourceClassName2, "EventSmallDesc", true); + descriptionCopy.SetFrom(sourceClassName3, "DescriptionCT"); + descriptionCopy.WithFieldPatch(f => + { + // for copied field, we also need to adjust field guid + f.Guid = GuidHelper.CreateFieldGuid($"DescriptionCopy"); // deterministic guid to ensure not conflicts occur in repeated migration + f.Caption = "Event description copy"; + }); + var teaser = m.BuildField("Teaser"); teaser.SetFrom(sourceClassName1, "EventTeaser", true); teaser.WithFieldPatch(f => f.Caption = "Event teaser"); @@ -162,16 +265,131 @@ public static IServiceCollection AddClassMergeExample(this IServiceCollection se var text = m.BuildField("Text"); text.SetFrom(sourceClassName1, "EventText", true); text.SetFrom(sourceClassName2, "EventHtml"); + text.SetFrom(sourceClassName3, "EventHtmlCT"); text.WithFieldPatch(f => f.Caption = "Event text"); var startDate = m.BuildField("StartDate"); startDate.SetFrom(sourceClassName1, "EventDateStart", true); // if needed use value conversion to adapt value - startDate.ConvertFrom(sourceClassName2, "EventStartDateAsText", false, - v => v?.ToString() is { } av && !string.IsNullOrWhiteSpace(av) ? DateTime.Parse(av) : null - ); + Func dateConvertor = (v, context) => + { + switch (context) + { + case ConvertorTreeNodeContext treeNodeContext: + // here you can use available treenode context + // (var nodeGuid, int nodeSiteId, int? documentId, bool migratingFromVersionHistory) = treeNodeContext; + break; + case ConvertorCustomTableContext customTableContext: + { + // handle any specific requirements if source data are of custom table class type + break; + } + default: + // no context is available (in future, mapping feature could be extended and therefore different context will be supplied or no context at all) + break; + } + + // be strict about value assertion and isolate any unexpected value + + return v switch + { + string stringValue => !string.IsNullOrWhiteSpace(stringValue) ? DateTime.Parse(stringValue) : null, // valid date or empty string isd expected, otherwise error + DateTime dateTimeValue => dateTimeValue, + null => null, + _ => throw new InvalidOperationException($"I didn't expected this value in instance of my class! {v}") + }; + }; + startDate.ConvertFrom(sourceClassName2, "EventStartDateAsText", false, dateConvertor); + startDate.ConvertFrom(sourceClassName3, "EventDateStartCT", false, dateConvertor); startDate.WithFieldPatch(f => f.Caption = "Event start date"); + // if desired add system field from custom table + var itemGuid = m.BuildField("EventGuid"); + itemGuid.SetFrom(sourceClassName3, "ItemGuid", true); + itemGuid.WithFieldPatch(f => + { + f.AllowEmpty = true; // allow empty, otherwise unmapped classes will throw + f.Caption = "Event guid"; + }); + + serviceCollection.AddSingleton(m); + + + + return serviceCollection; + } + + public sealed class SampleCustomTableHandler : DefaultCustomTableClassMappingHandler + { + private readonly ILogger logger; + + public SampleCustomTableHandler(ILogger logger) : base(logger) => this.logger = logger; + + public override void EnsureContentItemLanguageMetadata(ContentItemLanguageMetadataModel languageMetadataInfo, CustomTableMappingHandlerContext context) + { + // always call base if You want to use default fallbacks and guid derivation/generation + // if base is not called, You have to handle all fallbacks Yourself + base.EnsureContentItemLanguageMetadata(languageMetadataInfo, context); + + if (context.SourceClassName.Equals("_ET.EventCustomTable", StringComparison.InvariantCultureIgnoreCase)) + { + if (context.Values.TryGetValue("TitleCT", out object? mbTitle) && mbTitle is string titleCt) + { + languageMetadataInfo.ContentItemLanguageMetadataDisplayName = titleCt; + } + else + { + // no value assigned to my custom "TitleCT", i will leave fallback value generated with "DefaultCustomTableClassMappingHandler" + } + } + else + { + // unexpected dataclass? + logger.LogError("Unexpected data class '{ClassName}' in my mapping", context.SourceClassName); + } + } + } + + /// + /// This sample uses data from the official Kentico 11 e-shop demo + /// + public static IServiceCollection AddControlledCategoryMigrationSample(this IServiceCollection serviceCollection) + { + const string targetClassName = "Eshop.BookRemodeled"; + // declare target class + var m = new MultiClassMapping(targetClassName, target => + { + target.ClassName = targetClassName; + target.ClassTableName = "Eshop_BookRemodeled"; + target.ClassDisplayName = "Book remodeled"; + target.ClassType = ClassType.CONTENT_TYPE; + target.ClassContentTypeType = ClassContentTypeType.REUSABLE; + target.ClassWebPageHasUrl = false; + }); + + // set new primary key + m.BuildField("BookRemodeledID").AsPrimaryKey(); + + // change fields according to new requirements + const string sourceClassName1 = "CMSProduct.Book"; + + // field clone sample + m + .BuildField("BookAuthor") + .SetFrom(sourceClassName1, "BookAuthor", true) + .WithFieldPatch(f => f.SetPropertyValue(FormFieldPropertyEnum.FieldCaption, "Author's name")); + + m + .BuildField("BookISBN") + .SetFrom(sourceClassName1, "BookISBN", true) + .WithFieldPatch(f => f.Caption = "ISBN"); + + // category migration refinement sample + const int EbookCategoryID = 17; + int[] excludedCategories = [EbookCategoryID,]; + m.FilterCategories((className, categoryID) => !excludedCategories.Contains(categoryID)); + + // register class mapping serviceCollection.AddSingleton(m); return serviceCollection; diff --git a/Migration.Tool.Extensions/CommunityMigrations/SampleTextMigration.cs b/Migration.Tool.Extensions/CommunityMigrations/SampleTextMigration.cs index 4acf0c7f..0b9bbf29 100644 --- a/Migration.Tool.Extensions/CommunityMigrations/SampleTextMigration.cs +++ b/Migration.Tool.Extensions/CommunityMigrations/SampleTextMigration.cs @@ -47,7 +47,7 @@ public void MigrateFieldDefinition(FormDefinitionPatcher formDefinitionPatcher, public async Task MigrateValue(object? sourceValue, FieldMigrationContext context) { - // if required, migrate value (for example cut out any unsupported features or migrated them to kentico supported variants if available) + // if required, migrate value (for example cut out any unsupported features or migrated them to supported product variants if available) // check context if (context.SourceObjectContext is DocumentSourceObjectContext(_, _, _, _, _, _)) diff --git a/Migration.Tool.Extensions/README.md b/Migration.Tool.Extensions/README.md index 6b2cf2b7..89347f92 100644 --- a/Migration.Tool.Extensions/README.md +++ b/Migration.Tool.Extensions/README.md @@ -1,31 +1,47 @@ -## Custom migrations +# Custom migrations Samples: + - `Migration.Tool.Extensions/CommunityMigrations/SampleTextMigration.cs` contains simplest implementation for migration of text fields - `Migration.Tool.Extensions/DefaultMigrations/AssetMigration.cs` contains real world migration of assets (complex example) To create custom migration: -- create new file in `Migration.Tool.Extensions/CommunityMigrations` (directory if you need more files for single migration) -- implement interface `Migration.Tool.KXP.Api.Services.CmsClass.IFieldMigration` - - implement property rank, set number bellow 100 000 - for example 5000 - - implement method shall migrate (if method returns true, migration will be used) - - implement `MigrateFieldDefinition`, where objective is to mutate argument `XElement field` that represents one particular field - - implement `MigrateValue` where goal is to return new migrated value derived from `object? sourceValue` -- finally register in `Migration.Tool.Extensions/ServiceCollectionExtensions.cs` as `Transient` dependency into service collection. For example `services.AddTransient()` + +- Create new file in `Migration.Tool.Extensions/CommunityMigrations` (directory if you need more files for single migration) +- Implement interface `Migration.Tool.KXP.Api.Services.CmsClass.IFieldMigration` + - Implement property rank, set number bellow 100 000 - for example 5000 + - Implement method shall migrate (if method returns true, migration will be used) + - Implement `MigrateFieldDefinition`, where objective is to mutate argument `XElement field` that represents one particular field + - Implement `MigrateValue` where goal is to return new migrated value derived from `object? sourceValue` +- Register in `Migration.Tool.Extensions/ServiceCollectionExtensions.cs` as `Transient` dependency into service collection. For example `services.AddTransient()` ## Custom class mappings for page types -examples are `Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs` +Example code is found in `Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs`. ### Class remodeling sample -demonstrated in method `AddSimpleRemodelingSample`, goal is to take single data class and change it to more suitable shape. +Example code is found in the method `AddSimpleRemodelingSample`. + +The goal of this method is to take a **single data class** and change it to more suitable shape. ### Class merge sample -demonstrated in method `AddClassMergeExample`, goal is to take multiple data classes from source instance and define their relation to new class +Example code is found in the method `AddClassMergeExample`. + +The goal of this method is to take **multiple data classes** from the source instance and define their relation to a new class. + +### Class mapping with category control + +Example code is found in the method `AddK11EshopExample`. + +The goal of this method is to show how to **control migration of categories**. You can enable/disable the migration based on category ID and/or source class name. +This is useful when merging multiple data classes into one (see _Class merge sample_) + +### Example + +Let's define a new class: -lets define new class: ```csharp var m = new MultiClassMapping(targetClassName, target => { @@ -37,55 +53,80 @@ var m = new MultiClassMapping(targetClassName, target => }); ``` -define new primary key: +Then define a new primary key: + ```csharp m.BuildField("EventID").AsPrimaryKey(); ``` -and finally lets define relations to fields: - -1) build field title -```csharp -// build new field -var title = m.BuildField("Title"); - -// map "EventTitle" field form source data class "_ET.Event1" also use it as template for target field -title.SetFrom("_ET.Event1", "EventTitle", true); -// map "EventTitle" field form source data class "_ET.Event2" -title.SetFrom("_ET.Event2", "EventTitle"); +Finally, let's define relations to fields: + +1. build field title + + ```csharp + // build new field + var title = m.BuildField("Title"); + + // map "EventTitle" field form source data class "_ET.Event1" also use it as template for target field + title.SetFrom("_ET.Event1", "EventTitle", true); + // map "EventTitle" field form source data class "_ET.Event2" + title.SetFrom("_ET.Event2", "EventTitle"); + + // patch field definition, in this case lets change field caption + title.WithFieldPatch(f => f.Caption = "Event title"); + ``` + +1. in similar fashion map other fields +1. if needed custom value conversion can be used + + ```csharp + var startDate = m.BuildField("StartDate"); + startDate.SetFrom("_ET.Event1", "EventDateStart", true); + // if needed use value conversion to adapt value + startDate.ConvertFrom("_ET.Event2", "EventStartDateAsText", false, + (v, context) => + { + switch (context) + { + case ConvertorTreeNodeContext treeNodeContext: + // here you can use available treenode context + // (var nodeGuid, int nodeSiteId, int? documentId, bool migratingFromVersionHistory) = treeNodeContext; + break; + default: + // no context is available (possibly when tool is extended with other conversion possibilities) + break; + } + + return v?.ToString() is { } av && !string.IsNullOrWhiteSpace(av) ? DateTime.Parse(av) : null; + }); + startDate.WithFieldPatch(f => f.Caption = "Event start date"); + ``` + +1. register class mapping to dependency injection container + + ```csharp + serviceCollection.AddSingleton(m); + ``` -// patch field definition, in this case lets change field caption -title.WithFieldPatch(f => f.Caption = "Event title"); -``` +### Inject and use reusable schema -2) in similar fashion map other fields +Example code is found in the method `AddReusableSchemaIntegrationSample`. -3) if needed custom value conversion can be used -```csharp -var startDate = m.BuildField("StartDate"); -startDate.SetFrom("_ET.Event1", "EventDateStart", true); -// if needed use value conversion to adapt value -startDate.ConvertFrom("_ET.Event2", "EventStartDateAsText", false, - v => v?.ToString() is { } av && !string.IsNullOrWhiteSpace(av) ? DateTime.Parse(av) : null -); -startDate.WithFieldPatch(f => f.Caption = "Event start date"); -``` +The goal of this method is to take a **single data class** and assign reusable schema. -4) register class mapping to dependency injection ocntainer -```csharp -serviceCollection.AddSingleton(m); -``` +### Convert page type to reusable content item (content hub) -### Inject and use reusable schema +Example code is found in the method `AddReusableRemodelingSample`. -demonstrated in method `AddReusableSchemaIntegrationSample`, goal is to take single data class and assign reusable schema. +Please note, that all information unique to page will be lost. ## Custom widget migrations -Custom widget migration allows you to remodel the original widget as a new widget type. The prominent operations are +Custom widget migration allows you to remodel the original widget as a new widget type. The prominent operations are changing the target widget type and recombining the original properties. To create custom widget migration: + - create new file in `Migration.Tool.Extensions/CommunityMigrations` (directory if you need more files for single migration) - implement interface `Migration.Tool.KXP.Api.Services.CmsClass.IWidgetMigration` - implement property `Rank`, set number bellow 100 000 - for example 5000. Rank determines the order by which the migrations are tested to be eligible via the `ShallMigrate` method @@ -97,27 +138,25 @@ To create custom widget migration: - In the special case when you introduce a new property whose name overlaps with original property. Otherwise the migration infered from the original property would be used - If your new property is not supposed to be subject to property migrations and the original one was, explicitly specify `WidgetNoOpMigration` for this property - You can also override the property migration of an original property if that suits your case - -- finally register in `Migration.Tool.Extensions/ServiceCollectionExtensions.cs` as `Transient` dependency into service collection. For example `services.AddTransient()` +- Register in `Migration.Tool.Extensions/ServiceCollectionExtensions.cs` as `Transient` dependency into service collection. For example `services.AddTransient()` Samples: -- [Sample BannerWidget migration](./CommunityMigrations/SampleWidgetMigration.cs) -### Convert page type to reusable content item (content hub) - -demonstrated in method `AddReusableRemodelingSample`. Please note, that all information unique to page will be lost +- [Sample BannerWidget migration](./CommunityMigrations/SampleWidgetMigration.cs) ## Custom widget property migrations To create custom widget property migration: -- create new file in `Migration.Tool.Extensions/CommunityMigrations` (directory if you need more files for single migration) -- implement interface `Migration.Tool.KXP.Api.Services.CmsClass.IWidgetPropertyMigration` - - implement property `Rank`, set number bellow 100 000 - for example 5000. Rank determines the order by which the migrations are tested to be eligible via the `ShallMigrate` method - - implement method `ShallMigrate` (if method returns true, migration will be used) - - implement `MigrateWidgetProperty`, where objective is to convert old JToken representing json value to new converted JToken value -- finally register in `Migration.Tool.Extensions/ServiceCollectionExtensions.cs` as `Transient` dependency into service collection. For example `services.AddTransient()` -Samples: +- Create new file in `Migration.Tool.Extensions/CommunityMigrations` (directory if you need more files for single migration) +- Implement interface `Migration.Tool.KXP.Api.Services.CmsClass.IWidgetPropertyMigration` + - Implement property `Rank`, set number bellow 100 000 - for example 5000. Rank determines the order by which the migrations are tested to be eligible via the `ShallMigrate` method + - Implement method `ShallMigrate` (if method returns true, migration will be used) + - Implement `MigrateWidgetProperty`, where objective is to convert old JToken representing json value to new converted JToken value +- Register in `Migration.Tool.Extensions/ServiceCollectionExtensions.cs` as `Transient` dependency into service collection. For example `services.AddTransient()` + +### Widget migration samples + - [Path selector migration](./DefaultMigrations/WidgetPathSelectorMigration.cs) - [Page selector migration](./DefaultMigrations/WidgetPageSelectorMigration.cs) -- [File selector migration](./DefaultMigrations/WidgetFileMigration.cs) \ No newline at end of file +- [File selector migration](./DefaultMigrations/WidgetFileMigration.cs) diff --git a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs index 569d89d6..c8b72d53 100644 --- a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs +++ b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs @@ -18,6 +18,7 @@ public static IServiceCollection UseCustomizations(this IServiceCollection servi // services.AddClassMergeExample(); + // services.AddClassMergeExampleAsReusable(); // services.AddSimpleRemodelingSample(); // services.AddReusableRemodelingSample(); // services.AddReusableSchemaIntegrationSample(); diff --git a/Migration.Tool.KXP.Extensions/README.MD b/Migration.Tool.KXP.Extensions/README.MD index b21b8e62..be296b0c 100644 --- a/Migration.Tool.KXP.Extensions/README.MD +++ b/Migration.Tool.KXP.Extensions/README.MD @@ -1,4 +1,4 @@ -## Handler for legacy attachment URLs +# Handler for legacy attachment URLs Page attachments are not supported in Xperience by Kentico. Instead, the [Migration tool](/README.md) transfers attachment files into [media libraries](https://docs.xperience.io/x/agKiCQ). @@ -8,7 +8,7 @@ handler URLs and will no longer work (return 404 errors). For example, this can If you wish to continue using these legacy attachment URLs, you need to [add a custom handler](#add-the-handler-to-your-project) to your Xperience by Kentico project. -### Prerequisite +## Prerequisite To ensure that the handler provided in the `LegacyAttachmentHandler.cs` file works correctly, you need to [migrate](/Migration.Toolkit.CLI/README.md) your page attachments into a **media library** using the `MigrateMediaToMediaLibrary` configuration option, and **keep the default folder structure and file names**. By default, attachments are migrated into the content hub and the handler is not applicable. @@ -16,14 +16,14 @@ To ensure that the handler provided in the `LegacyAttachmentHandler.cs` file wor The `LegacyAttachmentHandler.cs` handler can serve the following legacy attachment URLs: -* `/getimage/[AttachmentGUID]/[AttachmentName].[AttachmentExtension]` - * Example: `/getimage/454BD7D7-200F-4976-AD21-8B5D70CBE7FD/image.jpg` -* `/getattachment/[NodeAliasPath]/[AttachmentName].[AttachmentExtension]` - * Example: `/getattachment/news/article1/image.jpg` -* `/getattachment/[AttachmentGUID]/[AttachmentName].[AttachmentExtension]` - * Example: `/getattachment/454BD7D7-200F-4976-AD21-8B5D70CBE7FD/image.jpg` +- `/getimage/[AttachmentGUID]/[AttachmentName].[AttachmentExtension]` + - Example: `/getimage/454BD7D7-200F-4976-AD21-8B5D70CBE7FD/image.jpg` +- `/getattachment/[NodeAliasPath]/[AttachmentName].[AttachmentExtension]` + - Example: `/getattachment/news/article1/image.jpg` +- `/getattachment/[AttachmentGUID]/[AttachmentName].[AttachmentExtension]` + - Example: `/getattachment/454BD7D7-200F-4976-AD21-8B5D70CBE7FD/image.jpg` -### Add the handler to your project +## Add the handler to your project 1. Open your Xperience by Kentico solution in Visual Studio. 2. Add a custom assembly (_Class Library_ project) to your solution or re-use an existing one. See diff --git a/Migration.Tool.Model/DataClassModel.cs b/Migration.Tool.Model/DataClassModel.cs deleted file mode 100644 index b2b13bf4..00000000 --- a/Migration.Tool.Model/DataClassModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Migration.Tool.Model; - -public class DataClassModel -{ - public virtual int ClassID { get; set; } - public virtual string ClassDisplayName { get; set; } - - public string ClassXmlSchema { get; set; } - public string ClassSearchSettings { get; set; } - public string ClassCodeGenerationSettings { get; set; } - public int ClassFormLayoutType { get; set; } -} \ No newline at end of file diff --git a/Migration.Tool.Model/Migration.Tool.Model.csproj b/Migration.Tool.Model/Migration.Tool.Model.csproj deleted file mode 100644 index eb2460e9..00000000 --- a/Migration.Tool.Model/Migration.Tool.Model.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net6.0 - enable - enable - - - diff --git a/README.md b/README.md index 87833e4f..7ed0edd9 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,17 @@ This repository is part of the [Xperience by Kentico Migration Toolkit](https:// The Kentico Migration Tool transfers content and other data from **Kentico Xperience 13**, **Kentico 12** or **Kentico 11** to **Xperience by Kentico**. -This repository contains several README documents containing information necessary for the correct usage of the Kentico Migration Tool. Namely: +This tool supports migrating a project to Xperience by Kentico over multiple migration runs with built-in and custom data transformations. -- [Migration CLI](./Migration.Toolkit.CLI/README.md) -- information about the necessary set up before running the Kentico Migration Tool. -- [Usage Guide](./docs/Usage-Guide.md) -- information about what kind of projects the Kentico Migration Tool supports. -- [Supported Data](./docs/Supported-Data.md) -- lists all available data for migration -- [Migration Protocol](./Migration.Toolkit.CLI/MIGRATION_PROTOCOL_REFERENCE.md) -- provides information about the results of the migration and required manual steps, etc. +Our documentation includes guides covering [the migration process from Kentico Xperience 13 to Xperience by Kentico](https://docs.kentico.com/x/migrate_from_kx13_guides). ## Library Version Matrix +View all [project releases](https://github.com/Kentico/xperience-by-kentico-kentico-migration-tool/releases/). + | Xperience Version | Library Version | -| ----------------- | --------------- | +|-------------------|-----------------| +| 29.7.0 | 1.6.0 | | 29.6.0 | 1.4.0 | | 29.5.2 | 1.3.0 | | 29.3.3 | 1.2.0 | @@ -36,12 +36,18 @@ This repository contains several README documents containing information necessa Follow the steps below to run the Kentico Migration Tool: -1. Clone or download the Migration.Tool source code from this repository. -2. Open the `Migration.Tool.sln` solution in Visual Studio. -3. Configure the options in the `Migration.Tool.CLI/appsettings.json` configuration file. See [`Migration.Tool.CLI/README.md - Configuration`](./Migration.Tool.CLI/README.md#Configuration) for details. -4. Rebuild the solution and restore all required NuGet packages. -5. Open the command line prompt. -6. Navigate to the output directory of the `Migration.Tool.CLI` project. (under `.\Migration.Toolkit.CLI\bin\Debug\net8.0\`) +1. Clone or download source code from this repository. +2. Open the `.\Migration.Tool.sln` solution in your IDE. +3. Configure the options in the `.\Migration.Tool.CLI\appsettings.json` configuration file. + + - See [`Migration.Tool.CLI/README.md - Configuration`](./Migration.Tool.CLI/README.md#Configuration) for details. + +4. Build the solution. +5. Open the the repository folder [in a terminal](https://github.com/microsoft/terminal). +6. Navigate to the output directory of the `Migration.Tool.CLI` project. + + - `.\Migration.Toolkit.CLI\bin\Debug\net8.0` + 7. Run the `Migration.Tool.CLI.exe migrate` command. - The following example shows the command with all parameters for complete migration: @@ -50,34 +56,29 @@ Follow the steps below to run the Kentico Migration Tool: .\Migration.Tool.CLI.exe migrate --sites --custom-modules --users --settings-keys --page-types --pages --attachments --contact-management --forms --media-libraries --data-protection --countries --custom-tables --members --categories ``` - - You can migrate your projects iteratively. For repeated runs bypass depency checks by using the `--bypass-dependency-check` parameter, if you know that required dependencies were already migrated succesfully. +8. Review the command line output of the tool. -8. Observe the command line output. The command output is also stored in a log file (`logs\log-.txt` under the output directory by default), which you can review later. -9. Review the migration protocol, which provides information about the result of the migration, lists required manual steps, etc. + - The output is also logged to a file `logs\log-.txt` under the output directory by default. - - You can find the protocol in the location specified by the `MigrationProtocolPath` key in the `appsettings.json` configuration file. - - For more information, see [`Migration.Tool.CLI/MIGRATION_PROTOCOL_REFERENCE.md`](./Migration.Tool.CLI/MIGRATION_PROTOCOL_REFERENCE.md). +9. Review the migration protocol output, which provides information about the result of the migration, lists required manual steps, etc. -The data is now migrated to the target Xperience by Kentico instance according to your configuration. See [`Migration.Tool.CLI/README.md`](./Migration.Tool.CLI/README.md) for detailed information about the migration CLI, configuration options, instructions related to individual object types, and manual migration steps. + - The output file path is found in the `Migration.Tool.CLI/appsettings.json` configuration file under the `MigrationProtocolPath` setting. -## Full Requirements +The data is now migrated to the target Xperience by Kentico instance according to your configuration. -View the [Usage Guide](./docs/Usage-Guide.md) for information about what kind of projects the Kentico Migration Tool supports. +## Full Requirements -## Changelog of recent updates +This repository contains several README documents containing information necessary for the correct usage of the Kentico Migration Tool. -- **September 4, 2024** - - Migration of media libraries and attachments to assets is available - - Media libraries and attachments are now [migrated](/Migration.Toolkit.CLI/README.md#media-libraries) to content item assets by default -- **June 13, 2024** - - Migration of categories to taxonomies is available -- **March 11, 2024** - - Kentico Xperience 11 instances are supported as a source of migration -- **February 1, 2024** - - Kentico Xperience 12 instances are supported as a source of migration +- [Migration CLI](./Migration.Toolkit.CLI/README.md) - detailed information about the migration CLI, configuration options, instructions related to individual object types, and manual migration steps. +- [Usage Guide](./docs/Usage-Guide.md) - information about what kind of projects the Kentico Migration Tool supports. +- [Supported Data](./docs/Supported-Data.md) - lists all available data for migration +- [Migration Protocol](./Migration.Toolkit.CLI/MIGRATION_PROTOCOL_REFERENCE.md) - provides information about the results of the migration and required manual steps, etc. ## Contributing +If you are [creating an issue](https://github.com/Kentico/xperience-by-kentico-kentico-migration-tool/issues/new) please provide all available information about the problem or error. If possible, include the command line output log file and migration protocol generated for your `Migration.Tool.CLI.exe migrate` command. + To see the guidelines for Contributing to Kentico open source software, please see [Kentico's `CONTRIBUTING.md`](https://github.com/Kentico/.github/blob/main/CONTRIBUTING.md) for more information and follow the [Kentico's `CODE_OF_CONDUCT`](https://github.com/Kentico/.github/blob/main/CODE_OF_CONDUCT.md). Instructions and technical details for contributing to **this** project can be found in [Contributing Setup](./docs/Contributing-Setup.md).