@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 +6 -2
- package/lib/commands/cm/stacks/import.js +11 -2
- package/lib/config/index.js +1 -0
- package/lib/import/module-importer.js +1 -1
- package/lib/import/modules/base-class.d.ts +1 -1
- package/lib/import/modules/entries.d.ts +9 -0
- package/lib/import/modules/entries.js +162 -7
- package/lib/import/modules/extensions.d.ts +7 -6
- package/lib/import/modules/extensions.js +85 -19
- package/lib/import/modules/global-fields.d.ts +8 -0
- package/lib/import/modules/global-fields.js +62 -5
- package/lib/import/modules/locales.js +7 -2
- package/lib/types/default-config.d.ts +1 -0
- package/lib/types/import-config.d.ts +2 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +0 -2
- package/lib/utils/common-helper.js +5 -16
- package/lib/utils/import-config-handler.js +5 -0
- package/oclif.manifest.json +19 -1
- package/package.json +5 -5
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
|
-
[](https://github.com/contentstack/cli/blob/main/LICENSE)
|
|
5
|
+
[](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.
|
|
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]';
|
package/lib/config/index.js
CHANGED
|
@@ -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(
|
|
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
|
|
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:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 =
|
|
67
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
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
|
-
|
|
106
|
-
|
|
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({
|
package/lib/types/index.d.ts
CHANGED
package/lib/types/index.js
CHANGED
|
@@ -98,22 +98,11 @@ const sanitizeStack = (importConfig) => {
|
|
|
98
98
|
};
|
|
99
99
|
exports.sanitizeStack = sanitizeStack;
|
|
100
100
|
const masterLocalDetails = (stackAPIClient) => {
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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;
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
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.
|
|
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
|
+
}
|