@kontent-ai/migration-toolkit 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +10 -9
  2. package/dist/es2022/core/logs/loggers.js +4 -1
  3. package/dist/es2022/core/logs/loggers.js.map +1 -1
  4. package/dist/es2022/core/models/core.models.d.ts +12 -0
  5. package/dist/es2022/core/models/log.models.d.ts +1 -1
  6. package/dist/es2022/core/models/migration.schema.d.ts +12 -12
  7. package/dist/es2022/core/models/migration.schema.js +2 -2
  8. package/dist/es2022/core/models/migration.schema.js.map +1 -1
  9. package/dist/es2022/core/utils/binary-data.utils.d.ts +4 -4
  10. package/dist/es2022/core/utils/binary-data.utils.js +2 -2
  11. package/dist/es2022/core/utils/binary-data.utils.js.map +1 -1
  12. package/dist/es2022/core/utils/global.utils.d.ts +3 -3
  13. package/dist/es2022/core/utils/global.utils.js +1 -1
  14. package/dist/es2022/core/utils/global.utils.js.map +1 -1
  15. package/dist/es2022/core/utils/http.utils.js +0 -3
  16. package/dist/es2022/core/utils/http.utils.js.map +1 -1
  17. package/dist/es2022/core/utils/processing-utils.d.ts +3 -3
  18. package/dist/es2022/core/utils/processing-utils.js +31 -4
  19. package/dist/es2022/core/utils/processing-utils.js.map +1 -1
  20. package/dist/es2022/export/context/export-context-fetcher.js +13 -7
  21. package/dist/es2022/export/context/export-context-fetcher.js.map +1 -1
  22. package/dist/es2022/export/export-manager.js +5 -4
  23. package/dist/es2022/export/export-manager.js.map +1 -1
  24. package/dist/es2022/file/file-manager.d.ts +3 -2
  25. package/dist/es2022/file/file-manager.js.map +1 -1
  26. package/dist/es2022/import/context/import-context-fetcher.js +14 -8
  27. package/dist/es2022/import/context/import-context-fetcher.js.map +1 -1
  28. package/dist/es2022/import/import-manager.d.ts +4 -0
  29. package/dist/es2022/import/import-manager.js +133 -4
  30. package/dist/es2022/import/import-manager.js.map +1 -1
  31. package/dist/es2022/import/import.models.d.ts +15 -5
  32. package/dist/es2022/import/importers/assets-importer.js.map +1 -1
  33. package/dist/es2022/import/importers/content-items-importer.d.ts +3 -3
  34. package/dist/es2022/import/importers/content-items-importer.js +11 -5
  35. package/dist/es2022/import/importers/content-items-importer.js.map +1 -1
  36. package/dist/es2022/import/importers/language-variant-importer.d.ts +4 -4
  37. package/dist/es2022/import/importers/language-variant-importer.js +4 -4
  38. package/dist/es2022/import/importers/language-variant-importer.js.map +1 -1
  39. package/dist/es2022/metadata.js +2 -2
  40. package/dist/es2022/node/cli/actions/import-action.js +3 -1
  41. package/dist/es2022/node/cli/actions/import-action.js.map +1 -1
  42. package/dist/es2022/toolkit/file.js +2 -2
  43. package/dist/es2022/toolkit/file.js.map +1 -1
  44. package/dist/es2022/toolkit/import.js +14 -2
  45. package/dist/es2022/toolkit/import.js.map +1 -1
  46. package/dist/es2022/translation/extraction/items-extraction.processor.d.ts +1 -1
  47. package/dist/es2022/translation/extraction/items-extraction.processor.js.map +1 -1
  48. package/dist/es2022/zip/zip-manager.d.ts +2 -2
  49. package/dist/es2022/zip/zip-manager.js.map +1 -1
  50. package/dist/es2022/zip/zip-packager.js +3 -4
  51. package/dist/es2022/zip/zip-packager.js.map +1 -1
  52. package/dist/es2022/zip/zip-transformer.js.map +1 -1
  53. package/dist/es2022/zip/zip.models.d.ts +4 -4
  54. package/lib/core/logs/loggers.ts +5 -1
  55. package/lib/core/models/core.models.ts +16 -0
  56. package/lib/core/models/log.models.ts +1 -1
  57. package/lib/core/models/migration.schema.ts +2 -2
  58. package/lib/core/utils/binary-data.utils.ts +6 -6
  59. package/lib/core/utils/global.utils.ts +8 -5
  60. package/lib/core/utils/http.utils.ts +1 -4
  61. package/lib/core/utils/processing-utils.ts +48 -18
  62. package/lib/export/context/export-context-fetcher.ts +67 -60
  63. package/lib/export/export-manager.ts +58 -56
  64. package/lib/file/file-manager.ts +4 -3
  65. package/lib/import/context/import-context-fetcher.ts +18 -13
  66. package/lib/import/import-manager.ts +174 -10
  67. package/lib/import/import.models.ts +20 -4
  68. package/lib/import/importers/assets-importer.ts +3 -9
  69. package/lib/import/importers/content-items-importer.ts +18 -17
  70. package/lib/import/importers/language-variant-importer.ts +32 -34
  71. package/lib/metadata.ts +2 -2
  72. package/lib/node/cli/actions/import-action.ts +3 -1
  73. package/lib/toolkit/file.ts +2 -2
  74. package/lib/toolkit/import.ts +16 -2
  75. package/lib/translation/extraction/items-extraction.processor.ts +3 -3
  76. package/lib/zip/zip-manager.ts +2 -2
  77. package/lib/zip/zip-packager.ts +3 -4
  78. package/lib/zip/zip-transformer.ts +1 -1
  79. package/lib/zip/zip.models.ts +3 -3
  80. package/package.json +13 -13
@@ -45,8 +45,8 @@ export function exportManager(config: ExportConfig) {
45
45
  codename: exportItem.workflow.codename
46
46
  }
47
47
  },
48
- versions: exportItem.versions.map((version) => {
49
- return <MigrationItemVersion>{
48
+ versions: exportItem.versions.map<MigrationItemVersion>((version) => {
49
+ return {
50
50
  elements: getMigrationElements(context, exportItem.contentType, version.languageVariant.elements),
51
51
  schedule: {
52
52
  publish_time: version.languageVariant.schedule.publishTime ?? undefined,
@@ -124,10 +124,10 @@ export function exportManager(config: ExportConfig) {
124
124
  };
125
125
 
126
126
  const getMigrationElementToStore = (data: {
127
- context: ExportContext;
128
- contentType: FlattenedContentType;
129
- typeElement: FlattenedContentTypeElement;
130
- exportElement: ElementModels.ContentItemElement;
127
+ readonly context: ExportContext;
128
+ readonly contentType: FlattenedContentType;
129
+ readonly typeElement: FlattenedContentTypeElement;
130
+ readonly exportElement: ElementModels.ContentItemElement;
131
131
  }): MigrationElementTransformData => {
132
132
  try {
133
133
  return exportTransforms[data.typeElement.type]({
@@ -175,60 +175,62 @@ export function exportManager(config: ExportConfig) {
175
175
  message: `Preparing to download '${chalk.yellow(assets.length.toString())}' assets`
176
176
  });
177
177
 
178
- return await processItemsAsync<Readonly<AssetModels.Asset>, MigrationAsset>({
179
- action: 'Downloading assets',
180
- logger: logger,
181
- parallelLimit: 5,
182
- itemInfo: (input) => {
183
- return {
184
- title: input.codename,
185
- itemType: 'asset'
186
- };
187
- },
188
- items: assets,
189
- processAsync: async (asset, logSpinner) => {
190
- const assetCollection: Readonly<CollectionModels.Collection> | undefined = context.environmentData.collections.find(
191
- (m) => m.id === asset.collection?.reference?.id
192
- );
193
- const assetFolder: Readonly<AssetFolderModels.AssetFolder> | undefined = context.environmentData.assetFolders.find(
194
- (m) => m.id === asset.folder?.id
195
- );
178
+ return (
179
+ await processItemsAsync<Readonly<AssetModels.Asset>, MigrationAsset>({
180
+ action: 'Downloading assets',
181
+ logger: logger,
182
+ parallelLimit: 5,
183
+ itemInfo: (input) => {
184
+ return {
185
+ title: input.codename,
186
+ itemType: 'asset'
187
+ };
188
+ },
189
+ items: assets,
190
+ processAsync: async (asset, logSpinner) => {
191
+ const assetCollection: Readonly<CollectionModels.Collection> | undefined = context.environmentData.collections.find(
192
+ (m) => m.id === asset.collection?.reference?.id
193
+ );
194
+ const assetFolder: Readonly<AssetFolderModels.AssetFolder> | undefined = context.environmentData.assetFolders.find(
195
+ (m) => m.id === asset.folder?.id
196
+ );
196
197
 
197
- logSpinner({
198
- type: 'download',
199
- message: `${asset.url}`
200
- });
198
+ logSpinner({
199
+ type: 'download',
200
+ message: `${asset.url}`
201
+ });
201
202
 
202
- const binaryData = await getBinaryDataFromUrlAsync(asset.url);
203
+ const migrationAsset: MigrationAsset = {
204
+ filename: asset.fileName,
205
+ title: asset.title ?? '',
206
+ codename: asset.codename,
207
+ binary_data: (await getBinaryDataFromUrlAsync(asset.url)).data,
208
+ collection: assetCollection ? { codename: assetCollection.codename } : undefined,
209
+ folder: assetFolder ? { codename: assetFolder.codename } : undefined,
210
+ descriptions: asset.descriptions.map((description) => {
211
+ const language = findRequired(
212
+ context.environmentData.languages,
213
+ (language) => language.id === description.language.id,
214
+ `Could not find language with id '${chalk.red(description.language.id)}' requested by asset '${chalk.red(
215
+ asset.codename
216
+ )}'`
217
+ );
203
218
 
204
- const migrationAsset: MigrationAsset = {
205
- filename: asset.fileName,
206
- title: asset.title ?? '',
207
- codename: asset.codename,
208
- binary_data: binaryData.data,
209
- collection: assetCollection ? { codename: assetCollection.codename } : undefined,
210
- folder: assetFolder ? { codename: assetFolder.codename } : undefined,
211
- descriptions: asset.descriptions.map((description) => {
212
- const language = findRequired(
213
- context.environmentData.languages,
214
- (language) => language.id === description.language.id,
215
- `Could not find language with id '${chalk.red(description.language.id)}' requested by asset '${chalk.red(
216
- asset.codename
217
- )}'`
218
- );
219
+ return {
220
+ description: description.description ?? undefined,
221
+ language: {
222
+ codename: language.codename
223
+ }
224
+ };
225
+ })
226
+ };
219
227
 
220
- return {
221
- description: description.description ?? undefined,
222
- language: {
223
- codename: language.codename
224
- }
225
- };
226
- })
227
- };
228
-
229
- return migrationAsset;
230
- }
231
- });
228
+ return migrationAsset;
229
+ }
230
+ })
231
+ )
232
+ .filter((m) => m.state === 'valid')
233
+ .map((m) => m.outputItem);
232
234
  };
233
235
 
234
236
  return {
@@ -1,13 +1,14 @@
1
+ import { Buffer as BufferProxy } from 'buffer';
1
2
  import chalk from 'chalk';
2
- import { Logger } from '../core/index.js';
3
3
  import { promises } from 'fs';
4
+ import { Logger } from '../core/index.js';
4
5
 
5
6
  export function fileManager(logger: Logger) {
6
7
  const getFilePath = (filename: string): string => {
7
8
  return `./${filename}`;
8
9
  };
9
10
 
10
- const loadFileAsync = async (filename: string): Promise<Buffer> => {
11
+ const loadFileAsync = async (filename: string): Promise<BufferProxy> => {
11
12
  const filePath = getFilePath(filename);
12
13
 
13
14
  logger.log({
@@ -20,7 +21,7 @@ export function fileManager(logger: Logger) {
20
21
  return file;
21
22
  };
22
23
 
23
- const writeFileAsync = async (fileNameWithoutExtension: string, content: string | Buffer): Promise<void> => {
24
+ const writeFileAsync = async (fileNameWithoutExtension: string, content: string | BufferProxy): Promise<void> => {
24
25
  const filePath = getFilePath(fileNameWithoutExtension);
25
26
 
26
27
  logger.log({
@@ -1,4 +1,6 @@
1
1
  import { AssetModels, ContentItemModels, LanguageVariantModels } from '@kontent-ai/management-sdk';
2
+ import chalk from 'chalk';
3
+ import { match } from 'ts-pattern';
2
4
  import {
3
5
  AssetStateInTargetEnvironmentByCodename,
4
6
  ItemStateInTargetEnvironmentByCodename,
@@ -12,16 +14,13 @@ import {
12
14
  WorkflowStep,
13
15
  findRequired,
14
16
  is404Error,
15
- isNotUndefined,
16
17
  managementClientUtils,
17
18
  processItemsAsync,
18
19
  runMapiRequestAsync,
19
20
  workflowHelper as workflowHelperInit
20
21
  } from '../../core/index.js';
21
- import { GetFlattenedElementByCodenames, ImportContext, ImportContextConfig, ImportContextEnvironmentData } from '../import.models.js';
22
22
  import { ExtractItemByCodename, itemsExtractionProcessor } from '../../translation/index.js';
23
- import chalk from 'chalk';
24
- import { match } from 'ts-pattern';
23
+ import { GetFlattenedElementByCodenames, ImportContext, ImportContextConfig, ImportContextEnvironmentData } from '../import.models.js';
25
24
 
26
25
  interface LanguageVariantWrapper {
27
26
  readonly draftLanguageVariant: Readonly<LanguageVariantModels.ContentItemLanguageVariant> | undefined;
@@ -150,7 +149,7 @@ export async function importContextFetcherAsync(config: ImportContextConfig) {
150
149
  migrationItems: readonly MigrationItem[]
151
150
  ): Promise<readonly LanguageVariantWrapper[]> => {
152
151
  return (
153
- await processItemsAsync<MigrationItem, LanguageVariantWrapper | undefined>({
152
+ await processItemsAsync<MigrationItem, LanguageVariantWrapper>({
154
153
  action: 'Fetching language variants',
155
154
  logger: config.logger,
156
155
  parallelLimit: 1,
@@ -166,7 +165,7 @@ export async function importContextFetcherAsync(config: ImportContextConfig) {
166
165
 
167
166
  if (!latestLanguageVariant) {
168
167
  // there is neither published or draft version as latest version does not exist at all
169
- return undefined;
168
+ return '404';
170
169
  }
171
170
 
172
171
  if (workflowHelper.isPublishedStepById(latestLanguageVariant.workflow.stepIdentifier.id ?? '')) {
@@ -186,14 +185,16 @@ export async function importContextFetcherAsync(config: ImportContextConfig) {
186
185
  };
187
186
  }
188
187
  })
189
- ).filter(isNotUndefined);
188
+ )
189
+ .filter((m) => m.state === 'valid')
190
+ .map((m) => m.outputItem);
190
191
  };
191
192
 
192
193
  const getContentItemsByCodenamesAsync = async (
193
194
  itemCodenames: ReadonlySet<string>
194
195
  ): Promise<readonly ContentItemModels.ContentItem[]> => {
195
196
  return (
196
- await processItemsAsync<string, Readonly<ContentItemModels.ContentItem> | undefined>({
197
+ await processItemsAsync<string, Readonly<ContentItemModels.ContentItem>>({
197
198
  action: 'Fetching content items',
198
199
  logger: config.logger,
199
200
  parallelLimit: 1,
@@ -219,16 +220,18 @@ export async function importContextFetcherAsync(config: ImportContextConfig) {
219
220
  throw error;
220
221
  }
221
222
 
222
- return undefined;
223
+ return '404';
223
224
  }
224
225
  }
225
226
  })
226
- ).filter(isNotUndefined);
227
+ )
228
+ .filter((m) => m.state === 'valid')
229
+ .map((m) => m.outputItem);
227
230
  };
228
231
 
229
232
  const getAssetsByCodenamesAsync = async (assetCodenames: ReadonlySet<string>): Promise<readonly AssetModels.Asset[]> => {
230
233
  return (
231
- await processItemsAsync<string, Readonly<AssetModels.Asset> | undefined>({
234
+ await processItemsAsync<string, Readonly<AssetModels.Asset>>({
232
235
  action: 'Fetching assets',
233
236
  logger: config.logger,
234
237
  parallelLimit: 1,
@@ -254,11 +257,13 @@ export async function importContextFetcherAsync(config: ImportContextConfig) {
254
257
  throw error;
255
258
  }
256
259
 
257
- return undefined;
260
+ return '404';
258
261
  }
259
262
  }
260
263
  })
261
- ).filter(isNotUndefined);
264
+ )
265
+ .filter((m) => m.state === 'valid')
266
+ .map((m) => m.outputItem);
262
267
  };
263
268
 
264
269
  const getVariantState = (languageVariant: Readonly<LanguageVariantModels.ContentItemLanguageVariant>): LanguageVariantStateData => {
@@ -1,12 +1,42 @@
1
- import { ContentItemModels, LanguageVariantModels, ManagementClient } from '@kontent-ai/management-sdk';
2
-
3
- import { defaultExternalIdGenerator, getDefaultLogger, getMigrationManagementClient, Logger } from '../core/index.js';
1
+ import { ManagementClient } from '@kontent-ai/management-sdk';
2
+ import chalk from 'chalk';
3
+ import { defaultExternalIdGenerator, extractErrorData, getDefaultLogger, getMigrationManagementClient, Logger } from '../core/index.js';
4
4
  import { importContextFetcherAsync } from './context/import-context-fetcher.js';
5
- import { ImportConfig, ImportContext, ImportResult } from './import.models.js';
5
+ import { ImportConfig, ImportContext, ImportedItem, ImportedLanguageVariant, ImportResult } from './import.models.js';
6
6
  import { assetsImporter } from './importers/assets-importer.js';
7
7
  import { contentItemsImporter } from './importers/content-items-importer.js';
8
8
  import { languageVariantImporter } from './importers/language-variant-importer.js';
9
9
 
10
+ const reportFilename: string = `import-report.json`;
11
+
12
+ type ReportResult = {
13
+ readonly errorsCount: number;
14
+ readonly assets: {
15
+ readonly count: number;
16
+ readonly successful: Array<{ readonly codename: string }>;
17
+ readonly failed: Array<{ readonly codename: string; readonly error: string }>;
18
+ };
19
+ readonly languageVariants: {
20
+ readonly count: number;
21
+ readonly successful: Array<{
22
+ readonly codename: string;
23
+ readonly language: { readonly codename: string };
24
+ readonly type: { readonly codename: string };
25
+ }>;
26
+ readonly failed: Array<{
27
+ readonly codename: string;
28
+ readonly language: { readonly codename: string };
29
+ readonly type: { readonly codename: string };
30
+ readonly error: string;
31
+ }>;
32
+ };
33
+ readonly contentItems: {
34
+ readonly count: number;
35
+ readonly successful: Array<{ readonly codename: string }>;
36
+ readonly failed: Array<{ readonly codename: string; readonly type: { readonly codename: string }; readonly error: string }>;
37
+ };
38
+ };
39
+
10
40
  export function importManager(config: ImportConfig) {
11
41
  const logger: Logger = config.logger ?? getDefaultLogger();
12
42
  const targetEnvironmentClient: Readonly<ManagementClient> = getMigrationManagementClient(config);
@@ -29,7 +59,8 @@ export function importManager(config: ImportConfig) {
29
59
  logger: logger
30
60
  }).importAsync();
31
61
  };
32
- const importContentItemsAsync = async (importContext: ImportContext): Promise<readonly Readonly<ContentItemModels.ContentItem>[]> => {
62
+
63
+ const importContentItemsAsync = async (importContext: ImportContext): Promise<readonly ImportedItem[]> => {
33
64
  if (!importContext.categorizedImportData.contentItems.length) {
34
65
  logger.log({
35
66
  type: 'info',
@@ -46,8 +77,8 @@ export function importManager(config: ImportConfig) {
46
77
 
47
78
  const importLanguageVariantsAsync = async (
48
79
  importContext: ImportContext,
49
- contentItems: readonly Readonly<ContentItemModels.ContentItem>[]
50
- ): Promise<readonly Readonly<LanguageVariantModels.ContentItemLanguageVariant>[]> => {
80
+ contentItems: readonly ImportedItem[]
81
+ ): Promise<readonly ImportedLanguageVariant[]> => {
51
82
  if (!importContext.categorizedImportData.contentItems.length) {
52
83
  logger.log({
53
84
  type: 'info',
@@ -63,6 +94,116 @@ export function importManager(config: ImportConfig) {
63
94
  }).importAsync();
64
95
  };
65
96
 
97
+ const getReportResult = (importResult: ImportResult): ReportResult => {
98
+ return {
99
+ errorsCount:
100
+ importResult.editedAssets.filter((m) => m.state === 'error').length +
101
+ importResult.uploadedAssets.filter((m) => m.state === 'error').length +
102
+ importResult.contentItems.filter((m) => m.state === 'error').length +
103
+ importResult.languageVariants.filter((m) => m.state === 'error').length,
104
+ assets: {
105
+ count: importResult.uploadedAssets.length + importResult.editedAssets.length,
106
+ successful: [
107
+ ...importResult.uploadedAssets.filter((m) => m.state === 'valid').map((m) => m.inputItem.codename),
108
+ ...importResult.editedAssets.filter((m) => m.state === 'valid').map((m) => m.inputItem.migrationAsset.codename)
109
+ ].map((m) => {
110
+ return {
111
+ codename: m
112
+ };
113
+ }),
114
+ failed: [
115
+ ...importResult.uploadedAssets
116
+ .filter((m) => m.state === 'error')
117
+ .map((m) => {
118
+ return {
119
+ codename: m.inputItem.codename,
120
+ error: extractErrorData(m.state === 'error').message
121
+ };
122
+ }),
123
+ ...importResult.editedAssets
124
+ .filter((m) => m.state === 'error')
125
+ .map((m) => {
126
+ return {
127
+ codename: m.inputItem.migrationAsset.codename,
128
+ error: extractErrorData(m.state === 'error').message
129
+ };
130
+ })
131
+ ]
132
+ },
133
+ contentItems: {
134
+ count: importResult.contentItems.length,
135
+ successful: importResult.contentItems
136
+ .filter((m) => m.state === 'valid')
137
+ .map((m) => {
138
+ return {
139
+ codename: m.inputItem.system.codename
140
+ };
141
+ }),
142
+ failed: importResult.contentItems
143
+ .filter((m) => m.state === 'error')
144
+ .map((m) => {
145
+ return {
146
+ codename: m.inputItem.system.codename,
147
+ type: m.inputItem.system.type,
148
+ error: extractErrorData(m.state === 'error').message
149
+ };
150
+ })
151
+ },
152
+ languageVariants: {
153
+ count: importResult.languageVariants.length,
154
+ successful: importResult.languageVariants
155
+ .filter((m) => m.state === 'valid')
156
+ .map((m) => {
157
+ return {
158
+ codename: m.inputItem.system.codename,
159
+ language: m.inputItem.system.language,
160
+ type: m.inputItem.system.type
161
+ };
162
+ }),
163
+ failed: importResult.languageVariants
164
+ .filter((m) => m.state === 'error')
165
+ .map((m) => {
166
+ return {
167
+ codename: m.inputItem.system.codename,
168
+ language: m.inputItem.system.language,
169
+ type: m.inputItem.system.type,
170
+ error: extractErrorData(m.state === 'error').message
171
+ };
172
+ })
173
+ }
174
+ };
175
+ };
176
+
177
+ const printReportToConsole = (reportResult: ReportResult, logger: Logger): void => {
178
+ const errors = [
179
+ ...reportResult.assets.failed.map((m) => [
180
+ `Object type: ${chalk.yellow('Asset')}`,
181
+ `Codename: ${chalk.yellow(m.codename)}`,
182
+ `${chalk.red(m.error)}`
183
+ ]),
184
+ ...reportResult.contentItems.failed.map((m) => [
185
+ `Object type: ${chalk.yellow('Content item')}`,
186
+ `Codename:${chalk.yellow(m.codename)}`,
187
+ `Content Type: ${chalk.yellow(m.type.codename)}`,
188
+ `${chalk.red(m.error)}`
189
+ ]),
190
+ ...reportResult.languageVariants.failed.map((m) => [
191
+ `Object type: ${chalk.yellow('Language variant')}`,
192
+ `Codename: ${chalk.yellow(m.codename)}`,
193
+ `Language: ${chalk.yellow(m.language.codename)}`,
194
+ `Content Type: ${chalk.yellow(m.type.codename)}`,
195
+ `${chalk.red(m.error)}`
196
+ ])
197
+ ];
198
+
199
+ errors.forEach((error, index) => {
200
+ logger.log({ message: `${chalk.red(`\nError #${index + 1}`)}` });
201
+ error.forEach((m) => {
202
+ logger.log({ message: m });
203
+ });
204
+ });
205
+ };
206
+
66
207
  return {
67
208
  async importAsync(): Promise<ImportResult> {
68
209
  const importContext = await (
@@ -83,17 +224,40 @@ export function importManager(config: ImportConfig) {
83
224
  // #3 Language variants
84
225
  const languageVariants = await importLanguageVariantsAsync(importContext, contentItems);
85
226
 
86
- logger.log({
87
- type: 'completed',
88
- message: `Finished import`
227
+ const reportResult = getReportResult({
228
+ contentItems,
229
+ editedAssets,
230
+ languageVariants,
231
+ uploadedAssets
89
232
  });
90
233
 
234
+ if (reportResult.errorsCount) {
235
+ printReportToConsole(reportResult, logger);
236
+ logger.log({
237
+ type: 'completed',
238
+ message: `Finished import with '${chalk.red(reportResult.errorsCount)}' ${
239
+ reportResult.errorsCount === 1 ? 'error' : 'errors'
240
+ }`
241
+ });
242
+ } else {
243
+ logger.log({
244
+ type: 'completed',
245
+ message: `Finished import`
246
+ });
247
+ }
248
+
91
249
  return {
92
250
  editedAssets,
93
251
  uploadedAssets,
94
252
  contentItems,
95
253
  languageVariants
96
254
  };
255
+ },
256
+ getReportFile(importResult: ImportResult): { readonly filename: string; readonly content: string } {
257
+ return {
258
+ filename: reportFilename,
259
+ content: JSON.stringify(getReportResult(importResult))
260
+ };
97
261
  }
98
262
  };
99
263
  }
@@ -14,6 +14,7 @@ import {
14
14
  ExternalIdGenerator,
15
15
  FlattenedContentType,
16
16
  FlattenedContentTypeElement,
17
+ ItemProcessingResult,
17
18
  ItemStateInTargetEnvironmentByCodename,
18
19
  LanguageVariantStateInTargetEnvironmentByCodename,
19
20
  Logger,
@@ -75,12 +76,27 @@ export type ImportTransformFunc = (data: {
75
76
  export interface ImportConfig extends ManagementClientConfig {
76
77
  readonly data: MigrationData;
77
78
  readonly externalIdGenerator?: ExternalIdGenerator;
79
+ readonly createReportFile?: boolean;
78
80
  readonly logger?: Logger;
79
81
  }
80
82
 
83
+ export interface AssetToEdit {
84
+ readonly migrationAsset: MigrationAsset;
85
+ readonly targetAsset: Readonly<AssetModels.Asset>;
86
+ readonly replaceBinaryFile: boolean;
87
+ }
88
+
89
+ export type ImportedItem = ItemProcessingResult<MigrationItem, Readonly<ContentItemModels.ContentItem>>;
90
+ export type ImportedLanguageVariant = ItemProcessingResult<
91
+ MigrationItem,
92
+ readonly Readonly<LanguageVariantModels.ContentItemLanguageVariant>[]
93
+ >;
94
+ export type EditedAsset = ItemProcessingResult<AssetToEdit, Readonly<AssetModels.Asset>>;
95
+ export type ImportedAsset = ItemProcessingResult<MigrationAsset, Readonly<AssetModels.Asset>>;
96
+
81
97
  export interface ImportResult {
82
- readonly uploadedAssets: readonly Readonly<AssetModels.Asset>[];
83
- readonly editedAssets: readonly Readonly<AssetModels.Asset>[];
84
- readonly contentItems: readonly Readonly<ContentItemModels.ContentItem>[];
85
- readonly languageVariants: readonly Readonly<LanguageVariantModels.ContentItemLanguageVariant>[];
98
+ readonly uploadedAssets: readonly ImportedAsset[];
99
+ readonly editedAssets: readonly EditedAsset[];
100
+ readonly contentItems: readonly ImportedItem[];
101
+ readonly languageVariants: readonly ImportedLanguageVariant[];
86
102
  }
@@ -13,13 +13,7 @@ import {
13
13
  runMapiRequestAsync
14
14
  } from '../../core/index.js';
15
15
  import { shouldReplaceBinaryFile, shouldUpdateAsset } from '../comparers/asset-comparer.js';
16
- import { ImportContext, ImportResult } from '../import.models.js';
17
-
18
- interface AssetToEdit {
19
- migrationAsset: MigrationAsset;
20
- targetAsset: Readonly<AssetModels.Asset>;
21
- replaceBinaryFile: boolean;
22
- }
16
+ import { AssetToEdit, EditedAsset, ImportContext, ImportedAsset, ImportResult } from '../import.models.js';
23
17
 
24
18
  export function assetsImporter(data: {
25
19
  readonly logger: Logger;
@@ -70,7 +64,7 @@ export function assetsImporter(data: {
70
64
  .filter(isNotUndefined);
71
65
  };
72
66
 
73
- const editAssets = async (assetsToEdit: readonly AssetToEdit[]): Promise<readonly Readonly<AssetModels.Asset>[]> => {
67
+ const editAssets = async (assetsToEdit: readonly AssetToEdit[]): Promise<readonly EditedAsset[]> => {
74
68
  data.logger.log({
75
69
  type: 'upsert',
76
70
  message: `Upserting '${chalk.yellow(assetsToEdit.length.toString())}' assets`
@@ -189,7 +183,7 @@ export function assetsImporter(data: {
189
183
  });
190
184
  };
191
185
 
192
- const uploadAssetsAsync = async (assetsToUpload: readonly MigrationAsset[]): Promise<readonly Readonly<AssetModels.Asset>[]> => {
186
+ const uploadAssetsAsync = async (assetsToUpload: readonly MigrationAsset[]): Promise<readonly ImportedAsset[]> => {
193
187
  data.logger.log({
194
188
  type: 'upload',
195
189
  message: `Uploading '${chalk.yellow(assetsToUpload.length.toString())}' assets`
@@ -1,14 +1,7 @@
1
1
  import { ContentItemModels, ManagementClient } from '@kontent-ai/management-sdk';
2
- import {
3
- Logger,
4
- processItemsAsync,
5
- runMapiRequestAsync,
6
- MigrationItem,
7
- LogSpinnerData,
8
- findRequired
9
- } from '../../core/index.js';
10
2
  import chalk from 'chalk';
11
- import { ImportContext } from '../import.models.js';
3
+ import { LogSpinnerData, Logger, MigrationItem, findRequired, processItemsAsync, runMapiRequestAsync } from '../../core/index.js';
4
+ import { ImportContext, ImportedItem } from '../import.models.js';
12
5
 
13
6
  export function contentItemsImporter(data: {
14
7
  readonly logger: Logger;
@@ -26,8 +19,7 @@ export function contentItemsImporter(data: {
26
19
  );
27
20
 
28
21
  return (
29
- migrationContentItem.system.name !== contentItem.name ||
30
- migrationContentItem.system.collection.codename !== collection.codename
22
+ migrationContentItem.system.name !== contentItem.name || migrationContentItem.system.collection.codename !== collection.codename
31
23
  );
32
24
  };
33
25
 
@@ -35,9 +27,7 @@ export function contentItemsImporter(data: {
35
27
  logSpinner: LogSpinnerData,
36
28
  migrationContentItem: MigrationItem
37
29
  ): Promise<{ contentItem: Readonly<ContentItemModels.ContentItem>; status: 'created' | 'itemAlreadyExists' }> => {
38
- const itemStateInTargetEnv = data.importContext.getItemStateInTargetEnvironment(
39
- migrationContentItem.system.codename
40
- );
30
+ const itemStateInTargetEnv = data.importContext.getItemStateInTargetEnvironment(migrationContentItem.system.codename);
41
31
 
42
32
  if (itemStateInTargetEnv.state === 'exists' && itemStateInTargetEnv.item) {
43
33
  return {
@@ -111,8 +101,19 @@ export function contentItemsImporter(data: {
111
101
  return preparedContentItemResult.contentItem;
112
102
  };
113
103
 
114
- const importAsync = async (): Promise<readonly Readonly<ContentItemModels.ContentItem>[]> => {
115
- const contentItemsToImport = data.importContext.categorizedImportData.contentItems;
104
+ const importAsync = async (): Promise<readonly ImportedItem[]> => {
105
+ // Only import unique content items based on their codename. The input may contain items in various language variants which share
106
+ // the same underlying content item.
107
+ const contentItemsToImport = data.importContext.categorizedImportData.contentItems.reduce<Readonly<MigrationItem[]>>(
108
+ (filteredItems, item) => {
109
+ if (filteredItems.some((m) => m.system.codename === item.system.codename)) {
110
+ return filteredItems;
111
+ }
112
+
113
+ return [...filteredItems, item];
114
+ },
115
+ []
116
+ );
116
117
 
117
118
  data.logger.log({
118
119
  type: 'info',
@@ -127,7 +128,7 @@ export function contentItemsImporter(data: {
127
128
  itemInfo: (item) => {
128
129
  return {
129
130
  itemType: 'contentItem',
130
- title: `${item.system.codename} -> ${item.system.language.codename}`
131
+ title: `${item.system.codename} -> ${item.system.type.codename}`
131
132
  };
132
133
  },
133
134
  processAsync: async (item, logSpinner) => {