@contentstack/cli-cm-import 1.9.0 → 1.10.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  It is Contentstack’s CLI plugin to import content in the stack. To learn how to export and import content in Contentstack, refer to the [Migration guide](https://www.contentstack.com/docs/developers/cli/migration/).
4
4
 
5
- [![License](https://img.shields.io/npm/l/@contentstack/cli)](https://github.com/contentstack/cli/blob/main/LICENSE)
5
+ [![License](https://img.shields.io/npm/l/@contentstack/cli)](https://github.com/contentstack/cli/blob/main/LICENSE)it -m
6
6
 
7
7
  <!-- toc -->
8
8
  * [Usage](#usage)
@@ -47,7 +47,7 @@ $ npm install -g @contentstack/cli-cm-import
47
47
  $ csdx COMMAND
48
48
  running command...
49
49
  $ csdx (--version)
50
- @contentstack/cli-cm-import/1.9.0 linux-x64 node-v18.18.0
50
+ @contentstack/cli-cm-import/1.10.0 linux-x64 node-v18.18.2
51
51
  $ csdx --help [COMMAND]
52
52
  USAGE
53
53
  $ csdx COMMAND
@@ -81,6 +81,8 @@ FLAGS
81
81
  -y, --yes [optional] Override marketplace prompts
82
82
  --import-webhook-status=<option> [default: disable] [optional] Webhook state
83
83
  <options: disable|current>
84
+ --replace-existing Replaces the existing module in the target stack.
85
+ --skip-existing Skips the module exists warning messages.
84
86
 
85
87
  DESCRIPTION
86
88
  Import content from a stack
@@ -126,6 +128,8 @@ FLAGS
126
128
  -y, --yes [optional] Override marketplace prompts
127
129
  --import-webhook-status=<option> [default: disable] [optional] Webhook state
128
130
  <options: disable|current>
131
+ --replace-existing Replaces the existing module in the target stack.
132
+ --skip-existing Skips the module exists warning messages.
129
133
 
130
134
  DESCRIPTION
131
135
  Import content from a stack
@@ -11,14 +11,12 @@ class ImportCommand extends cli_command_1.Command {
11
11
  // setup import config
12
12
  // initialize the importer
13
13
  // start import
14
- let contentDir;
15
14
  let backupDir;
16
15
  try {
17
16
  const { flags } = await this.parse(ImportCommand);
18
17
  let importConfig = await (0, utils_1.setupImportConfig)(flags);
19
18
  // Note setting host to create cma client
20
19
  importConfig.host = this.cmaHost;
21
- contentDir = importConfig.contentDir;
22
20
  backupDir = importConfig.backupDir;
23
21
  const managementAPIClient = await (0, cli_utilities_1.managementSDKClient)(importConfig);
24
22
  const moduleImporter = new import_1.ModuleImporter(managementAPIClient, importConfig);
@@ -84,6 +82,7 @@ ImportCommand.flags = {
84
82
  parse: (0, cli_utilities_1.printFlagDeprecation)(['-A', '--auth-token']),
85
83
  }),
86
84
  module: cli_utilities_1.flags.string({
85
+ required: false,
87
86
  char: 'm',
88
87
  description: '[optional] specific module name',
89
88
  parse: (0, cli_utilities_1.printFlagDeprecation)(['-m'], ['--module']),
@@ -109,6 +108,16 @@ ImportCommand.flags = {
109
108
  required: false,
110
109
  description: '[optional] Override marketplace prompts',
111
110
  }),
111
+ 'replace-existing': cli_utilities_1.flags.boolean({
112
+ required: false,
113
+ description: 'Replaces the existing module in the target stack.',
114
+ dependsOn: ['module'],
115
+ }),
116
+ 'skip-existing': cli_utilities_1.flags.boolean({
117
+ required: false,
118
+ default: false,
119
+ description: 'Skips the module exists warning messages.',
120
+ }),
112
121
  };
113
122
  ImportCommand.aliases = ['cm:import'];
114
123
  ImportCommand.usage = 'cm:stacks:import [-c <value>] [-k <value>] [-d <value>] [-a <value>] [--module <value>] [--backup-dir <value>] [--branch <value>] [--import-webhook-status disable|current]';
@@ -367,6 +367,7 @@ const config = {
367
367
  stacks: '/stacks/',
368
368
  labels: '/labels/',
369
369
  },
370
+ overwriteSupportedModules: ['extensions', 'global-fields', 'content-types'],
370
371
  rateLimit: 5,
371
372
  preserveStackVersion: false,
372
373
  entriesPublish: true,
@@ -23,7 +23,7 @@ class ModuleImporter {
23
23
  const httpClient = new cli_utilities_1.HttpClient({
24
24
  headers: { api_key: this.importConfig.apiKey, authorization: this.importConfig.management_token },
25
25
  });
26
- const { data } = await httpClient.post('https://api.contentstack.io/v3/locales', {
26
+ const { data } = await httpClient.post(`https://${this.importConfig.host}/v3/locales`, {
27
27
  locale: {
28
28
  name: 'English',
29
29
  code: 'en-us',
@@ -3,7 +3,7 @@ import { ImportConfig, ModuleClassParams } from '../../types';
3
3
  export type AdditionalKeys = {
4
4
  backupDir: string;
5
5
  };
6
- export type ApiModuleType = 'create-assets' | 'replace-assets' | 'publish-assets' | 'create-assets-folder' | 'create-extensions' | 'create-locale' | 'update-locale' | 'create-gfs' | 'create-cts' | 'update-cts' | 'update-gfs' | 'create-environments' | 'create-labels' | 'update-labels' | 'create-webhooks' | 'create-workflows' | 'create-custom-role' | 'create-entries' | 'update-entries' | 'publish-entries' | 'delete-entries';
6
+ export type ApiModuleType = 'create-assets' | 'replace-assets' | 'publish-assets' | 'create-assets-folder' | 'create-extensions' | 'update-extensions' | 'create-locale' | 'update-locale' | 'create-gfs' | 'create-cts' | 'update-cts' | 'update-gfs' | 'create-environments' | 'create-labels' | 'update-labels' | 'create-webhooks' | 'create-workflows' | 'create-custom-role' | 'create-entries' | 'update-entries' | 'publish-entries' | 'delete-entries';
7
7
  export type ApiOptions = {
8
8
  uid?: string;
9
9
  url?: string;
@@ -57,6 +57,15 @@ export default class EntriesImport extends BaseClass {
57
57
  * @returns {ApiOptions} ApiOptions
58
58
  */
59
59
  serializeEntries(apiOptions: ApiOptions): ApiOptions;
60
+ replaceEntries({ cTUid, locale }: {
61
+ cTUid: string;
62
+ locale: string;
63
+ }): Promise<void>;
64
+ replaceEntriesHandler({ apiParams, element: entry, isLastRequest, }: {
65
+ apiParams: ApiOptions;
66
+ element: Record<string, string>;
67
+ isLastRequest: boolean;
68
+ }): Promise<unknown>;
60
69
  populateEntryUpdatePayload(): {
61
70
  cTUid: string;
62
71
  locale: string;
@@ -56,6 +56,14 @@ class EntriesImport extends base_class_1.default {
56
56
  for (let entryRequestOption of entryRequestOptions) {
57
57
  await this.createEntries(entryRequestOption);
58
58
  }
59
+ if (this.importConfig.replaceExisting && (0, lodash_1.indexOf)(this.importConfig.overwriteSupportedModules, 'entries') !== -1) {
60
+ // Note: Instead of using entryRequestOptions, we can prepare request options for replace, to avoid unnecessary operations
61
+ for (let entryRequestOption of entryRequestOptions) {
62
+ await this.replaceEntries(entryRequestOption).catch((error) => {
63
+ (0, utils_1.log)(this.importConfig, `Error while replacing the existing entries ${(0, utils_1.formatError)(error)}`, 'error');
64
+ });
65
+ }
66
+ }
59
67
  await utils_1.fileHelper.writeLargeFile(path.join(this.entriesMapperPath, 'uid-mapping.json'), this.entriesUidMapper); // TBD: manages mapper in one file, should find an alternative
60
68
  utils_1.fsUtil.writeFile(path.join(this.entriesMapperPath, 'failed-entries.json'), this.failedEntries);
61
69
  if (this.autoCreatedEntries.length > 0) {
@@ -129,7 +137,8 @@ class EntriesImport extends base_class_1.default {
129
137
  * @returns {ApiOptions} ApiOptions
130
138
  */
131
139
  serializeUpdateCTs(apiOptions) {
132
- const { apiData: contentType } = apiOptions;
140
+ const { apiData } = apiOptions;
141
+ const contentType = (0, lodash_1.cloneDeep)(apiData);
133
142
  if (contentType.field_rules) {
134
143
  delete contentType.field_rules;
135
144
  }
@@ -201,6 +210,15 @@ class EntriesImport extends base_class_1.default {
201
210
  keepMetadata: false,
202
211
  omitKeys: this.entriesConfig.invalidKeys,
203
212
  });
213
+ // create file instance for existing entries
214
+ const existingEntriesFileHelper = new cli_utilities_1.FsUtility({
215
+ moduleName: 'entries',
216
+ indexFileName: 'index.json',
217
+ basePath: path.join(this.entriesMapperPath, cTUid, locale, 'existing'),
218
+ chunkFileSize: this.entriesConfig.chunkFileSize,
219
+ keepMetadata: false,
220
+ omitKeys: this.entriesConfig.invalidKeys,
221
+ });
204
222
  const contentType = (0, lodash_1.find)(this.cTs, { uid: cTUid });
205
223
  const onSuccess = ({ response, apiData: entry, additionalInfo }) => {
206
224
  var _a, _b;
@@ -227,10 +245,33 @@ class EntriesImport extends base_class_1.default {
227
245
  entriesCreateFileHelper.writeIntoFile({ [entry.uid]: entry }, { mapKeyVal: true });
228
246
  }
229
247
  };
230
- const onReject = ({ error, apiData: { uid, title } }) => {
231
- (0, utils_1.log)(this.importConfig, `${title} entry of content type ${cTUid} in locale ${locale} failed to create`, 'error');
232
- (0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'error');
233
- this.failedEntries.push({ content_type: cTUid, locale, entry: { uid, title } });
248
+ const onReject = ({ error, apiData: entry, additionalInfo }) => {
249
+ var _a, _b;
250
+ const { title, uid } = entry;
251
+ //Note: write existing entries into files to handler later
252
+ if (error.errorCode === 119) {
253
+ if (((_a = error === null || error === void 0 ? void 0 : error.errors) === null || _a === void 0 ? void 0 : _a.title) || ((_b = error === null || error === void 0 ? void 0 : error.errors) === null || _b === void 0 ? void 0 : _b.uid)) {
254
+ if (this.importConfig.replaceExisting &&
255
+ (0, lodash_1.indexOf)(this.importConfig.overwriteSupportedModules, 'entries') !== -1) {
256
+ entry.entryOldUid = uid;
257
+ entry.sourceEntryFilePath = path.join(basePath, additionalInfo.entryFileName); // stores source file path temporarily
258
+ existingEntriesFileHelper.writeIntoFile({ [uid]: entry }, { mapKeyVal: true });
259
+ }
260
+ if (!this.importConfig.skipExisting) {
261
+ (0, utils_1.log)(this.importConfig, `Entry '${title}' already exists`, 'info');
262
+ }
263
+ }
264
+ else {
265
+ (0, utils_1.log)(this.importConfig, `${title} entry of content type ${cTUid} in locale ${locale} failed to create`, 'error');
266
+ (0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'error');
267
+ this.failedEntries.push({ content_type: cTUid, locale, entry: { uid, title } });
268
+ }
269
+ }
270
+ else {
271
+ (0, utils_1.log)(this.importConfig, `${title} entry of content type ${cTUid} in locale ${locale} failed to create`, 'error');
272
+ (0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'error');
273
+ this.failedEntries.push({ content_type: cTUid, locale, entry: { uid, title } });
274
+ }
234
275
  };
235
276
  for (const index in indexer) {
236
277
  const chunk = await fs.readChunkFiles.next().catch((error) => {
@@ -254,6 +295,7 @@ class EntriesImport extends base_class_1.default {
254
295
  concurrencyLimit: this.importConcurrency,
255
296
  }).then(() => {
256
297
  entriesCreateFileHelper === null || entriesCreateFileHelper === void 0 ? void 0 : entriesCreateFileHelper.completeFile(true);
298
+ existingEntriesFileHelper === null || existingEntriesFileHelper === void 0 ? void 0 : existingEntriesFileHelper.completeFile(true);
257
299
  (0, utils_1.log)(this.importConfig, `Created entries for content type ${cTUid} in locale ${locale}`, 'success');
258
300
  });
259
301
  }
@@ -287,7 +329,7 @@ class EntriesImport extends base_class_1.default {
287
329
  apiOptions.apiData = entryResponse;
288
330
  apiOptions.additionalInfo[entryResponse.uid] = {
289
331
  isLocalized: true,
290
- entryOldUid: entry.uid
332
+ entryOldUid: entry.uid,
291
333
  };
292
334
  return apiOptions;
293
335
  }
@@ -301,6 +343,118 @@ class EntriesImport extends base_class_1.default {
301
343
  }
302
344
  return apiOptions;
303
345
  }
346
+ async replaceEntries({ cTUid, locale }) {
347
+ const processName = 'Replace existing Entries';
348
+ const indexFileName = 'index.json';
349
+ const basePath = path.join(this.entriesMapperPath, cTUid, locale, 'existing');
350
+ const fs = new cli_utilities_1.FsUtility({ basePath, indexFileName });
351
+ const indexer = fs.indexFileContent;
352
+ const indexerCount = (0, lodash_1.values)(indexer).length;
353
+ if (indexerCount === 0) {
354
+ return Promise.resolve();
355
+ }
356
+ // Write updated entries
357
+ const entriesReplaceFileHelper = new cli_utilities_1.FsUtility({
358
+ moduleName: 'entries',
359
+ indexFileName: 'index.json',
360
+ basePath: path.join(this.entriesMapperPath, cTUid, locale),
361
+ chunkFileSize: this.entriesConfig.chunkFileSize,
362
+ keepMetadata: false,
363
+ useIndexer: true,
364
+ omitKeys: this.entriesConfig.invalidKeys,
365
+ });
366
+ // log(this.importConfig, `Starting to update entries with references for ${cTUid} in locale ${locale}`, 'info');
367
+ const contentType = (0, lodash_1.find)(this.cTs, { uid: cTUid });
368
+ const onSuccess = ({ response, apiData: entry, additionalInfo }) => {
369
+ (0, utils_1.log)(this.importConfig, `Replaced entry: '${entry.title}' of content type ${cTUid} in locale ${locale}`, 'info');
370
+ this.entriesUidMapper[entry.uid] = response.uid;
371
+ entriesReplaceFileHelper.writeIntoFile({ [entry.uid]: entry }, { mapKeyVal: true });
372
+ };
373
+ const onReject = ({ error, apiData: { uid, title } }) => {
374
+ (0, utils_1.log)(this.importConfig, `${title} entry of content type ${cTUid} in locale ${locale} failed to replace`, 'error');
375
+ (0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'error');
376
+ this.failedEntries.push({
377
+ content_type: cTUid,
378
+ locale,
379
+ entry: { uid: this.entriesUidMapper[uid], title },
380
+ entryId: uid,
381
+ });
382
+ };
383
+ for (const index in indexer) {
384
+ const chunk = await fs.readChunkFiles.next().catch((error) => {
385
+ (0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'error');
386
+ });
387
+ if (chunk) {
388
+ let apiContent = (0, lodash_1.values)(chunk);
389
+ await this.makeConcurrentCall({
390
+ apiContent,
391
+ processName,
392
+ indexerCount,
393
+ currentIndexer: +index,
394
+ apiParams: {
395
+ reject: onReject,
396
+ resolve: onSuccess,
397
+ entity: 'update-entries',
398
+ includeParamOnCompletion: true,
399
+ additionalInfo: { contentType, locale, cTUid },
400
+ },
401
+ concurrencyLimit: this.importConcurrency,
402
+ }, this.replaceEntriesHandler.bind(this)).then(() => {
403
+ entriesReplaceFileHelper === null || entriesReplaceFileHelper === void 0 ? void 0 : entriesReplaceFileHelper.completeFile(true);
404
+ (0, utils_1.log)(this.importConfig, `Replaced entries for content type ${cTUid} in locale ${locale}`, 'success');
405
+ });
406
+ }
407
+ }
408
+ }
409
+ async replaceEntriesHandler({ apiParams, element: entry, isLastRequest, }) {
410
+ const { additionalInfo: { cTUid, locale } = {} } = apiParams;
411
+ return new Promise(async (resolve, reject) => {
412
+ const { items: [entryInStack] = [] } = (await this.stack
413
+ .contentType(cTUid)
414
+ .entry()
415
+ .query({ query: { title: entry.title, locale } })
416
+ .findOne()
417
+ .catch((error) => {
418
+ apiParams.reject({
419
+ error,
420
+ apiData: entry,
421
+ });
422
+ reject(true);
423
+ })) || {};
424
+ if (entryInStack) {
425
+ const entryPayload = this.stack.contentType(cTUid).entry(entryInStack.uid);
426
+ Object.assign(entryPayload, entryInStack, (0, lodash_1.cloneDeep)(entry), {
427
+ uid: entryInStack.uid,
428
+ urlPath: entryInStack.urlPath,
429
+ stackHeaders: entryInStack.stackHeaders,
430
+ _version: entryInStack._version,
431
+ });
432
+ return entryPayload
433
+ .update({ locale })
434
+ .then((response) => {
435
+ apiParams.resolve({
436
+ response,
437
+ apiData: entry,
438
+ });
439
+ resolve(true);
440
+ })
441
+ .catch((error) => {
442
+ apiParams.reject({
443
+ error,
444
+ apiData: entry,
445
+ });
446
+ reject(true);
447
+ });
448
+ }
449
+ else {
450
+ apiParams.reject({
451
+ error: new Error(`Entry with title ${entry.title} not found in the stack`),
452
+ apiData: entry,
453
+ });
454
+ reject(true);
455
+ }
456
+ });
457
+ }
304
458
  populateEntryUpdatePayload() {
305
459
  const requestOptions = [];
306
460
  for (let locale of this.locales) {
@@ -428,7 +582,8 @@ class EntriesImport extends base_class_1.default {
428
582
  * @returns {ApiOptions} ApiOptions
429
583
  */
430
584
  serializeUpdateCTsWithRef(apiOptions) {
431
- const { apiData: contentType } = apiOptions;
585
+ const { apiData } = apiOptions;
586
+ const contentType = (0, lodash_1.cloneDeep)(apiData);
432
587
  if (contentType.field_rules) {
433
588
  delete contentType.field_rules;
434
589
  }
@@ -11,6 +11,7 @@ export default class ImportExtensions extends BaseClass {
11
11
  private extUidMapper;
12
12
  private extSuccess;
13
13
  private extFailed;
14
+ private existingExtensions;
14
15
  constructor({ importConfig, stackAPIClient }: ModuleClassParams);
15
16
  /**
16
17
  * @method start
@@ -18,10 +19,10 @@ export default class ImportExtensions extends BaseClass {
18
19
  */
19
20
  start(): Promise<void>;
20
21
  importExtensions(): Promise<any>;
21
- /**
22
- * @method serializeExtensions
23
- * @param {ApiOptions} apiOptions ApiOptions
24
- * @returns {ApiOptions} ApiOptions
25
- */
26
- serializeExtensions(apiOptions: ApiOptions): ApiOptions;
22
+ replaceExtensions(): Promise<any>;
23
+ replaceExtensionHandler({ apiParams, element: extension, isLastRequest, }: {
24
+ apiParams: ApiOptions;
25
+ element: Record<string, string>;
26
+ isLastRequest: boolean;
27
+ }): Promise<unknown>;
27
28
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const isEmpty_1 = tslib_1.__importDefault(require("lodash/isEmpty"));
5
5
  const values_1 = tslib_1.__importDefault(require("lodash/values"));
6
+ const cloneDeep_1 = tslib_1.__importDefault(require("lodash/cloneDeep"));
6
7
  const node_path_1 = require("node:path");
7
8
  const utils_1 = require("../../utils");
8
9
  const base_class_1 = tslib_1.__importDefault(require("./base-class"));
@@ -17,6 +18,7 @@ class ImportExtensions extends base_class_1.default {
17
18
  this.extFailsPath = (0, node_path_1.join)(this.mapperDirPath, 'fails.json');
18
19
  this.extFailed = [];
19
20
  this.extSuccess = [];
21
+ this.existingExtensions = [];
20
22
  this.extUidMapper = {};
21
23
  }
22
24
  /**
@@ -39,6 +41,12 @@ class ImportExtensions extends base_class_1.default {
39
41
  ? utils_1.fsUtil.readFile((0, node_path_1.join)(this.extUidMapperPath), true)
40
42
  : {};
41
43
  await this.importExtensions();
44
+ // Note: if any extensions present, then update it
45
+ if (this.importConfig.replaceExisting && this.existingExtensions.length > 0) {
46
+ await this.replaceExtensions().catch((error) => {
47
+ (0, utils_1.log)(this.importConfig, `Error while replacing extensions ${(0, utils_1.formatError)(error)}`, 'error');
48
+ });
49
+ }
42
50
  if ((_a = this.extSuccess) === null || _a === void 0 ? void 0 : _a.length) {
43
51
  utils_1.fsUtil.writeFile(this.extSuccessPath, this.extSuccess);
44
52
  }
@@ -61,10 +69,14 @@ class ImportExtensions extends base_class_1.default {
61
69
  };
62
70
  const onReject = ({ error, apiData }) => {
63
71
  var _a;
64
- const err = (error === null || error === void 0 ? void 0 : error.message) ? JSON.parse(error.message) : error;
65
72
  const { title } = apiData;
66
- if ((_a = err === null || err === void 0 ? void 0 : err.errors) === null || _a === void 0 ? void 0 : _a.title) {
67
- (0, utils_1.log)(this.importConfig, `Extension '${title}' already exists`, 'info');
73
+ if ((_a = error === null || error === void 0 ? void 0 : error.errors) === null || _a === void 0 ? void 0 : _a.title) {
74
+ if (this.importConfig.replaceExisting) {
75
+ this.existingExtensions.push(apiData);
76
+ }
77
+ if (!this.importConfig.skipExisting) {
78
+ (0, utils_1.log)(this.importConfig, `Extension '${title}' already exists`, 'info');
79
+ }
68
80
  }
69
81
  else {
70
82
  this.extFailed.push(apiData);
@@ -76,7 +88,6 @@ class ImportExtensions extends base_class_1.default {
76
88
  apiContent,
77
89
  processName: 'import extensions',
78
90
  apiParams: {
79
- serializeData: this.serializeExtensions.bind(this),
80
91
  reject: onReject.bind(this),
81
92
  resolve: onSuccess.bind(this),
82
93
  entity: 'create-extensions',
@@ -85,21 +96,76 @@ class ImportExtensions extends base_class_1.default {
85
96
  concurrencyLimit: this.importConfig.concurrency || this.importConfig.fetchConcurrency || 1,
86
97
  }, undefined, false);
87
98
  }
88
- /**
89
- * @method serializeExtensions
90
- * @param {ApiOptions} apiOptions ApiOptions
91
- * @returns {ApiOptions} ApiOptions
92
- */
93
- serializeExtensions(apiOptions) {
94
- const { apiData: extension } = apiOptions;
95
- if (this.extUidMapper.hasOwnProperty(extension.uid)) {
96
- (0, utils_1.log)(this.importConfig, `Extension '${extension.title}' already exists. Skipping it to avoid duplicates!`, 'info');
97
- apiOptions.entity = undefined;
98
- }
99
- else {
100
- apiOptions.apiData = extension;
101
- }
102
- return apiOptions;
99
+ async replaceExtensions() {
100
+ const onSuccess = ({ response, apiData: { uid, title } = { uid: null, title: '' } }) => {
101
+ this.extSuccess.push(response);
102
+ this.extUidMapper[uid] = response.uid;
103
+ (0, utils_1.log)(this.importConfig, `Extension '${title}' replaced successfully`, 'success');
104
+ utils_1.fsUtil.writeFile(this.extUidMapperPath, this.extUidMapper);
105
+ };
106
+ const onReject = ({ error, apiData }) => {
107
+ this.extFailed.push(apiData);
108
+ (0, utils_1.log)(this.importConfig, `Extension '${apiData.title}' failed to replace ${(0, utils_1.formatError)(error)}`, 'error');
109
+ (0, utils_1.log)(this.importConfig, error, 'error');
110
+ };
111
+ await this.makeConcurrentCall({
112
+ apiContent: this.existingExtensions,
113
+ processName: 'Replace extensions',
114
+ apiParams: {
115
+ reject: onReject.bind(this),
116
+ resolve: onSuccess.bind(this),
117
+ entity: 'update-extensions',
118
+ includeParamOnCompletion: true,
119
+ },
120
+ concurrencyLimit: this.importConfig.concurrency || this.importConfig.fetchConcurrency || 1,
121
+ }, this.replaceExtensionHandler.bind(this));
122
+ }
123
+ async replaceExtensionHandler({ apiParams, element: extension, isLastRequest, }) {
124
+ return new Promise(async (resolve, reject) => {
125
+ const { items: [extensionsInStack] = [] } = await this.stack
126
+ .extension()
127
+ .query({ query: { title: extension.title } })
128
+ .findOne()
129
+ .catch((error) => {
130
+ apiParams.reject({
131
+ error,
132
+ apiData: extension,
133
+ });
134
+ reject(true);
135
+ });
136
+ if (extensionsInStack) {
137
+ const extensionPayload = this.stack.extension(extension.uid);
138
+ Object.assign(extensionPayload, extensionsInStack, (0, cloneDeep_1.default)(extension), {
139
+ uid: extensionsInStack.uid,
140
+ urlPath: extensionsInStack.urlPath,
141
+ _version: extensionsInStack._version,
142
+ stackHeaders: extensionsInStack.stackHeaders,
143
+ });
144
+ return extensionPayload
145
+ .update()
146
+ .then((response) => {
147
+ apiParams.resolve({
148
+ response,
149
+ apiData: extension,
150
+ });
151
+ resolve(true);
152
+ })
153
+ .catch((error) => {
154
+ apiParams.reject({
155
+ error,
156
+ apiData: extension,
157
+ });
158
+ reject(true);
159
+ });
160
+ }
161
+ else {
162
+ apiParams.reject({
163
+ error: new Error(`Extension with title ${extension.title} not found in the stack`),
164
+ apiData: extension,
165
+ });
166
+ reject(true);
167
+ }
168
+ });
103
169
  }
104
170
  }
105
171
  exports.default = ImportExtensions;
@@ -22,6 +22,7 @@ export default class ImportGlobalFields extends BaseClass {
22
22
  private marketplaceAppMapperPath;
23
23
  private reqConcurrency;
24
24
  private installedExtensions;
25
+ private existingGFs;
25
26
  private gFsConfig;
26
27
  constructor({ importConfig, stackAPIClient }: ModuleClassParams);
27
28
  start(): Promise<any>;
@@ -31,4 +32,11 @@ export default class ImportGlobalFields extends BaseClass {
31
32
  element: Record<string, string>;
32
33
  isLastRequest: boolean;
33
34
  }): Promise<unknown>;
35
+ replaceGFs(): Promise<any>;
36
+ /**
37
+ * @method serializeUpdateGFs
38
+ * @param {ApiOptions} apiOptions ApiOptions
39
+ * @returns {ApiOptions} ApiOptions
40
+ */
41
+ serializeReplaceGFs(apiOptions: ApiOptions): ApiOptions;
34
42
  }
@@ -21,6 +21,7 @@ class ImportGlobalFields extends base_class_1.default {
21
21
  this.createdGFs = [];
22
22
  this.failedGFs = [];
23
23
  this.pendingGFs = [];
24
+ this.existingGFs = [];
24
25
  this.reqConcurrency = this.gFsConfig.writeConcurrency || this.config.writeConcurrency;
25
26
  this.gFsMapperPath = path.resolve(this.config.data, 'mapper', 'global_fields');
26
27
  this.gFsFolderPath = path.resolve(this.config.data, this.gFsConfig.dirName);
@@ -43,6 +44,11 @@ class ImportGlobalFields extends base_class_1.default {
43
44
  this.installedExtensions = ((await utils_1.fsUtil.readFile(this.marketplaceAppMapperPath)) || { extension_uid: {} }).extension_uid;
44
45
  await this.importGFs();
45
46
  utils_1.fsUtil.writeFile(this.gFsPendingPath, this.pendingGFs);
47
+ if (this.importConfig.replaceExisting && this.existingGFs.length > 0) {
48
+ await this.replaceGFs().catch((error) => {
49
+ (0, utils_1.log)(this.importConfig, `Error while replacing global fields ${(0, utils_1.formatError)(error)}`, 'error');
50
+ });
51
+ }
46
52
  (0, utils_1.log)(this.config, 'Global fields import has been completed!', 'info');
47
53
  }
48
54
  async importGFs() {
@@ -52,10 +58,22 @@ class ImportGlobalFields extends base_class_1.default {
52
58
  utils_1.fsUtil.writeFile(this.gFsUidMapperPath, this.gFsUidMapper);
53
59
  (0, utils_1.log)(this.config, 'Global field ' + uid + ' created successfully', 'success');
54
60
  };
55
- const onReject = ({ error, apiData: { uid } = undefined }) => {
56
- (0, utils_1.log)(this.importConfig, `Global fields '${uid}' failed to import`, 'error');
57
- (0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'error');
58
- this.failedGFs.push({ uid });
61
+ const onReject = ({ error, apiData: globalField = undefined }) => {
62
+ var _a;
63
+ const uid = globalField.uid;
64
+ if ((_a = error === null || error === void 0 ? void 0 : error.errors) === null || _a === void 0 ? void 0 : _a.title) {
65
+ if (this.importConfig.replaceExisting) {
66
+ this.existingGFs.push(globalField);
67
+ }
68
+ if (!this.importConfig.skipExisting) {
69
+ (0, utils_1.log)(this.importConfig, `Global fields '${uid}' already exist`, 'info');
70
+ }
71
+ }
72
+ else {
73
+ (0, utils_1.log)(this.importConfig, `Global fields '${uid}' failed to import`, 'error');
74
+ (0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'error');
75
+ this.failedGFs.push({ uid });
76
+ }
59
77
  };
60
78
  return await this.makeConcurrentCall({
61
79
  processName: 'Import global fields',
@@ -73,7 +91,7 @@ class ImportGlobalFields extends base_class_1.default {
73
91
  return new Promise(async (resolve, reject) => {
74
92
  (0, utils_1.lookupExtension)(this.config, globalField.schema, this.config.preserveStackVersion, this.installedExtensions);
75
93
  let flag = { supressed: false };
76
- await (0, utils_1.removeReferenceFields)(globalField.schema, flag, this.stackAPIClient);
94
+ await (0, utils_1.removeReferenceFields)(globalField.schema, flag, this.stack);
77
95
  if (flag.supressed) {
78
96
  this.pendingGFs.push(globalField.uid);
79
97
  }
@@ -96,5 +114,44 @@ class ImportGlobalFields extends base_class_1.default {
96
114
  });
97
115
  });
98
116
  }
117
+ async replaceGFs() {
118
+ const onSuccess = ({ response: globalField, apiData: { uid } = { uid: null } }) => {
119
+ this.createdGFs.push(globalField);
120
+ this.gFsUidMapper[uid] = globalField;
121
+ utils_1.fsUtil.writeFile(this.gFsUidMapperPath, this.gFsUidMapper);
122
+ (0, utils_1.log)(this.config, 'Global field ' + uid + ' replaced successfully', 'success');
123
+ };
124
+ const onReject = ({ error, apiData: { uid } }) => {
125
+ (0, utils_1.log)(this.importConfig, `Global fields '${uid}' failed to replace`, 'error');
126
+ (0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'error');
127
+ this.failedGFs.push({ uid });
128
+ };
129
+ await this.makeConcurrentCall({
130
+ apiContent: this.existingGFs,
131
+ processName: 'Replace global fields',
132
+ apiParams: {
133
+ serializeData: this.serializeReplaceGFs.bind(this),
134
+ reject: onReject.bind(this),
135
+ resolve: onSuccess.bind(this),
136
+ entity: 'update-gfs',
137
+ includeParamOnCompletion: true,
138
+ },
139
+ concurrencyLimit: this.importConfig.concurrency || this.importConfig.fetchConcurrency || 1,
140
+ }, undefined, false);
141
+ }
142
+ /**
143
+ * @method serializeUpdateGFs
144
+ * @param {ApiOptions} apiOptions ApiOptions
145
+ * @returns {ApiOptions} ApiOptions
146
+ */
147
+ serializeReplaceGFs(apiOptions) {
148
+ const { apiData: globalField } = apiOptions;
149
+ const globalFieldPayload = this.stack.globalField(globalField.uid);
150
+ Object.assign(globalFieldPayload, (0, lodash_1.cloneDeep)(globalField), {
151
+ stackHeaders: globalFieldPayload.stackHeaders,
152
+ });
153
+ apiOptions.apiData = globalFieldPayload;
154
+ return apiOptions;
155
+ }
99
156
  }
100
157
  exports.default = ImportGlobalFields;
@@ -102,8 +102,13 @@ class ImportLocales extends base_class_1.default {
102
102
  utils_1.fsUtil.writeFile(this.langUidMapperPath, this.langUidMapper);
103
103
  };
104
104
  const onReject = ({ error, apiData: { uid, code } = undefined }) => {
105
- (0, utils_1.log)(this.importConfig, `Language '${code}' failed to import`, 'error');
106
- (0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'error');
105
+ if ((error === null || error === void 0 ? void 0 : error.errorCode) === 247) {
106
+ (0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'info');
107
+ }
108
+ else {
109
+ (0, utils_1.log)(this.importConfig, `Language '${code}' failed to import`, 'error');
110
+ (0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'error');
111
+ }
107
112
  this.failedLocales.push({ uid, code });
108
113
  };
109
114
  return await this.makeConcurrentCall({
@@ -139,4 +139,5 @@ export default interface DefaultConfig {
139
139
  marketplaceAppEncryptionKey: string;
140
140
  getEncryptionKeyMaxRetry: number;
141
141
  createBackupDir?: string;
142
+ overwriteSupportedModules: string[];
142
143
  }
@@ -42,6 +42,8 @@ export default interface ImportConfig extends DefaultConfig, ExternalConfig {
42
42
  destinationStackName?: string;
43
43
  org_uid?: string;
44
44
  contentVersion: number;
45
+ replaceExisting?: boolean;
46
+ skipExisting?: boolean;
45
47
  }
46
48
  type branch = {
47
49
  uid: string;
@@ -29,6 +29,7 @@ export type ModuleClassParams = {
29
29
  export interface Extensions {
30
30
  dirName: string;
31
31
  fileName: string;
32
+ validKeys: string[];
32
33
  dependencies?: Modules[];
33
34
  }
34
35
  export interface MarketplaceAppsConfig {
@@ -1,4 +1,2 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- ;
4
- ;
@@ -98,22 +98,11 @@ const sanitizeStack = (importConfig) => {
98
98
  };
99
99
  exports.sanitizeStack = sanitizeStack;
100
100
  const masterLocalDetails = (stackAPIClient) => {
101
- return new Promise((resolve, reject) => {
102
- const result = stackAPIClient.locale().query();
103
- result
104
- .find()
105
- .then((response) => {
106
- const masterLocalObj = response.items.filter((obj) => {
107
- if (obj.fallback_locale === null) {
108
- return obj;
109
- }
110
- });
111
- return resolve(masterLocalObj[0]);
112
- })
113
- .catch((error) => {
114
- return reject(error);
115
- });
116
- });
101
+ return stackAPIClient
102
+ .locale()
103
+ .query({ query: { fallback_locale: null } })
104
+ .find()
105
+ .then(({ items }) => items[0]);
117
106
  };
118
107
  exports.masterLocalDetails = masterLocalDetails;
119
108
  const field_rules_update = (importConfig, ctPath) => {
@@ -75,6 +75,11 @@ const setupConfig = async (importCmdFlags) => {
75
75
  }
76
76
  // Note to support old modules
77
77
  config.target_stack = config.apiKey;
78
+ config.replaceExisting = importCmdFlags['replace-existing'];
79
+ config.skipExisting = importCmdFlags['skip-existing'];
80
+ if (config.replaceExisting && !(0, lodash_1.includes)(config.overwriteSupportedModules, config.moduleName)) {
81
+ throw new Error(`Failed to overwrite ${config.moduleName} module! Currently, with the import command, you can overwrite the following modules: ${config.overwriteSupportedModules.join(',')}`);
82
+ }
78
83
  return config;
79
84
  };
80
85
  exports.default = setupConfig;
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.9.0",
2
+ "version": "1.10.0",
3
3
  "commands": {
4
4
  "cm:stacks:import": {
5
5
  "id": "cm:stacks:import",
@@ -86,6 +86,7 @@
86
86
  "type": "option",
87
87
  "char": "m",
88
88
  "description": "[optional] specific module name",
89
+ "required": false,
89
90
  "multiple": false
90
91
  },
91
92
  "backup-dir": {
@@ -121,6 +122,23 @@
121
122
  "description": "[optional] Override marketplace prompts",
122
123
  "required": false,
123
124
  "allowNo": false
125
+ },
126
+ "replace-existing": {
127
+ "name": "replace-existing",
128
+ "type": "boolean",
129
+ "description": "Replaces the existing module in the target stack.",
130
+ "required": false,
131
+ "allowNo": false,
132
+ "dependsOn": [
133
+ "module"
134
+ ]
135
+ },
136
+ "skip-existing": {
137
+ "name": "skip-existing",
138
+ "type": "boolean",
139
+ "description": "Skips the module exists warning messages.",
140
+ "required": false,
141
+ "allowNo": false
124
142
  }
125
143
  },
126
144
  "args": {}
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@contentstack/cli-cm-import",
3
3
  "description": "Contentstack CLI plugin to import content into stack",
4
- "version": "1.9.0",
4
+ "version": "1.10.0",
5
5
  "author": "Contentstack",
6
6
  "bugs": "https://github.com/contentstack/cli/issues",
7
7
  "dependencies": {
8
+ "@contentstack/cli-command": "~1.2.14",
9
+ "@contentstack/cli-utilities": "~1.5.4",
8
10
  "@contentstack/management": "~1.10.2",
9
- "@contentstack/cli-command": "~1.2.12",
10
- "@contentstack/cli-utilities": "~1.5.2",
11
11
  "@oclif/core": "^2.9.3",
12
12
  "big-json": "^3.2.0",
13
13
  "bluebird": "^3.7.2",
@@ -92,8 +92,8 @@
92
92
  },
93
93
  "shortCommandName": {
94
94
  "cm:stacks:import": "IMPRT",
95
- "cm:import": "IMPRT"
95
+ "cm:import": "O-IMPRT"
96
96
  }
97
97
  },
98
98
  "repository": "https://github.com/contentstack/cli"
99
- }
99
+ }