@contentstack/cli-cm-import 1.0.1 → 1.2.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.
@@ -0,0 +1,443 @@
1
+ /*!
2
+ * Contentstack Export
3
+ * Copyright (c) 2019 Contentstack LLC
4
+ * MIT Licensed
5
+ */
6
+ const fs = require('fs');
7
+ const _ = require('lodash');
8
+ const path = require('path');
9
+ const chalk = require('chalk');
10
+ const mkdirp = require('mkdirp');
11
+ const { cliux, HttpClient, NodeCrypto } = require('@contentstack/cli-utilities');
12
+
13
+ let config = require('../../config/default');
14
+ const { addlogs: log } = require('../util/log');
15
+ const { readFileSync, writeFile } = require('../util/fs');
16
+ const sdk = require('../util/contentstack-management-sdk');
17
+ const { getDeveloperHubUrl, getInstalledExtensions } = require('../util/marketplace-app-helper');
18
+
19
+ module.exports = class ImportMarketplaceApps {
20
+ client;
21
+ marketplaceApps = [];
22
+ marketplaceAppsUid = [];
23
+ developerHubBaseUrl = null;
24
+ marketplaceAppFolderPath = '';
25
+ marketplaceAppConfig = config.modules.marketplace_apps;
26
+
27
+ constructor(credentialConfig) {
28
+ this.config = _.merge(config, credentialConfig);
29
+ }
30
+
31
+ async start() {
32
+ this.client = sdk.Client(this.config);
33
+ this.developerHubBaseUrl = await getDeveloperHubUrl();
34
+ this.marketplaceAppFolderPath = path.resolve(this.config.data, this.marketplaceAppConfig.dirName);
35
+ this.marketplaceApps = _.uniqBy(
36
+ readFileSync(path.resolve(this.marketplaceAppFolderPath, this.marketplaceAppConfig.fileName)),
37
+ 'app_uid',
38
+ );
39
+ this.marketplaceAppsUid = _.map(this.marketplaceApps, 'uid');
40
+
41
+ if (!this.config.auth_token && !_.isEmpty(this.marketplaceApps)) {
42
+ cliux.print(
43
+ 'WARNING!!! To import Marketplace apps, you must be logged in. Please check csdx auth:login --help to log in',
44
+ { color: 'yellow' },
45
+ );
46
+ return Promise.resolve();
47
+ } else if (_.isEmpty(this.marketplaceApps)) {
48
+ return Promise.resolve();
49
+ }
50
+
51
+ await this.getOrgUid();
52
+ return this.handleInstallationProcess();
53
+ }
54
+
55
+ /**
56
+ * @method getOrgUid
57
+ * @returns {Void}
58
+ */
59
+ getOrgUid = async () => {
60
+ const self = this;
61
+ // NOTE get org uid
62
+ if (self.config.auth_token) {
63
+ const stack = await this.client
64
+ .stack({ api_key: self.config.target_stack, authtoken: self.config.auth_token })
65
+ .fetch()
66
+ .catch((error) => {
67
+ console.log(error);
68
+ log(self.config, 'Starting marketplace app installation', 'success');
69
+ });
70
+
71
+ if (stack && stack.org_uid) {
72
+ self.config.org_uid = stack.org_uid;
73
+ }
74
+ }
75
+ };
76
+
77
+ /**
78
+ * @method handleInstallationProcess
79
+ * @returns {Promise<void>}
80
+ */
81
+ handleInstallationProcess = async () => {
82
+ const self = this;
83
+ const headers = {
84
+ authtoken: self.config.auth_token,
85
+ organization_uid: self.config.org_uid,
86
+ };
87
+ const httpClient = new HttpClient().headers(headers);
88
+ const nodeCrypto = new NodeCrypto();
89
+
90
+ // NOTE install all private apps which is not available for stack.
91
+ await this.handleAllPrivateAppsCreationProcess({ httpClient });
92
+ const installedExtensions = await getInstalledExtensions(self.config);
93
+
94
+ // NOTE after private app installation, refetch marketplace apps from file
95
+ const marketplaceAppsFromFile = readFileSync(
96
+ path.resolve(this.marketplaceAppFolderPath, self.marketplaceAppConfig.fileName),
97
+ );
98
+ this.marketplaceApps = _.filter(marketplaceAppsFromFile, ({ uid }) => _.includes(this.marketplaceAppsUid, uid));
99
+
100
+ log(self.config, 'Starting marketplace app installation', 'success');
101
+
102
+ for (let app of self.marketplaceApps) {
103
+ await self.installApps({ app, installedExtensions, httpClient, nodeCrypto });
104
+ }
105
+
106
+ // NOTE get all the extension again after all apps installed (To manage uid mapping in content type, entries)
107
+ const extensions = await getInstalledExtensions(self.config);
108
+ const mapperFolderPath = path.join(self.config.data, 'mapper', 'marketplace_apps');
109
+
110
+ if (!fs.existsSync(mapperFolderPath)) {
111
+ mkdirp.sync(mapperFolderPath);
112
+ }
113
+
114
+ const appUidMapperPath = path.join(mapperFolderPath, 'marketplace-apps.json');
115
+ const installedExt = _.map(extensions, (row) =>
116
+ _.pick(row, ['uid', 'title', 'type', 'app_uid', 'app_installation_uid']),
117
+ );
118
+
119
+ writeFile(appUidMapperPath, installedExt);
120
+
121
+ return Promise.resolve();
122
+ };
123
+
124
+ /**
125
+ * @method handleAllPrivateAppsCreationProcess
126
+ * @param {Object} options
127
+ * @returns {Promise<void>}
128
+ */
129
+ handleAllPrivateAppsCreationProcess = async (options) => {
130
+ const self = this;
131
+ const { httpClient } = options;
132
+ const listOfExportedPrivateApps = _.filter(self.marketplaceApps, { visibility: 'private' });
133
+
134
+ if (_.isEmpty(listOfExportedPrivateApps)) {
135
+ return Promise.resolve();
136
+ }
137
+
138
+ // NOTE get list of developer-hub installed apps (private)
139
+ const installedDeveloperHubApps =
140
+ (await httpClient
141
+ .get(`${this.developerHubBaseUrl}/apps`)
142
+ .then(({ data: { data } }) => data)
143
+ .catch((err) => {
144
+ console.log(err);
145
+ })) || [];
146
+ const listOfNotInstalledPrivateApps = _.filter(
147
+ listOfExportedPrivateApps,
148
+ (app) => !_.includes(_.map(installedDeveloperHubApps, 'uid'), app.app_uid),
149
+ );
150
+
151
+ if (!_.isEmpty(listOfNotInstalledPrivateApps) && !self.config.forceMarketplaceAppsImport) {
152
+ const confirmation = await cliux.confirm(
153
+ chalk.yellow(
154
+ `WARNING!!! The listed apps are private apps that are not available in the destination stack: \n\n${_.map(
155
+ listOfNotInstalledPrivateApps,
156
+ ({ manifest: { name } }, index) => `${String(index + 1)}) ${name}`,
157
+ ).join('\n')}\n\nWould you like to re-create the private app and then proceed with the installation? (y/n)`,
158
+ ),
159
+ );
160
+
161
+ if (!confirmation) {
162
+ const continueProcess = await cliux.confirm(
163
+ chalk.yellow(
164
+ `WARNING!!! Canceling the app re-creation may break the content type and entry import. Would you like to proceed? (y/n)`,
165
+ ),
166
+ );
167
+
168
+ if (continueProcess) {
169
+ return resolve();
170
+ } else {
171
+ process.exit();
172
+ }
173
+ }
174
+ }
175
+
176
+ log(self.config, 'Starting developer hub private apps re-creation', 'success');
177
+
178
+ for (let app of listOfNotInstalledPrivateApps) {
179
+ await self.createAllPrivateAppsInDeveloperHub({ app, httpClient });
180
+ }
181
+
182
+ return Promise.resolve();
183
+ };
184
+
185
+ /**
186
+ * @method removeUidFromManifestUILocations
187
+ * @param {Array<Object>} locations
188
+ * @returns {Array<Object>}
189
+ */
190
+ removeUidFromManifestUILocations = (locations) => {
191
+ return _.map(locations, (location) => {
192
+ if (location.meta) {
193
+ location.meta = _.map(location.meta, (meta) => _.omit(meta, ['uid']));
194
+ }
195
+
196
+ return location;
197
+ });
198
+ };
199
+
200
+ /**
201
+ * @method createAllPrivateAppsInDeveloperHub
202
+ * @param {Object} options
203
+ * @returns {Promise<void>}
204
+ */
205
+ createAllPrivateAppsInDeveloperHub = async (options, uidCleaned = false) => {
206
+ const self = this;
207
+ const { app, httpClient } = options;
208
+
209
+ return new Promise((resolve) => {
210
+ if (!uidCleaned && app.manifest.ui_location && !_.isEmpty(app.manifest.ui_location.locations)) {
211
+ app.manifest.ui_location.locations = this.removeUidFromManifestUILocations(app.manifest.ui_location.locations);
212
+ }
213
+
214
+ httpClient
215
+ .post(`${this.developerHubBaseUrl}/apps`, app.manifest)
216
+ .then(async ({ data: result }) => {
217
+ const { name } = app.manifest;
218
+ const { data, error, message } = result || {};
219
+
220
+ if (error) {
221
+ log(self.config, message, 'error');
222
+
223
+ if (_.toLower(error) === 'conflict') {
224
+ const appName = self.config.forceMarketplaceAppsImport
225
+ ? self.getAppName(app.manifest.name)
226
+ : await cliux.inquire({
227
+ type: 'input',
228
+ name: 'name',
229
+ default: `${app.manifest.name}-1`,
230
+ validate: this.validateAppName,
231
+ message: `${message}. Enter a new name to create an app.?`,
232
+ });
233
+ app.manifest.name = appName;
234
+
235
+ await self.createAllPrivateAppsInDeveloperHub({ app, httpClient }, true).then(resolve).catch(resolve);
236
+ } else {
237
+ if (self.config.forceMarketplaceAppsImport) return resolve();
238
+
239
+ const confirmation = await cliux.confirm(
240
+ chalk.yellow(
241
+ 'WARNING!!! The above error may have an impact if the failed app is referenced in entries/content type. Would you like to proceed? (y/n)',
242
+ ),
243
+ );
244
+
245
+ if (confirmation) {
246
+ resolve();
247
+ } else {
248
+ process.exit();
249
+ }
250
+ }
251
+ } else if (data) {
252
+ // NOTE new app installation
253
+ log(self.config, `${name} app created successfully.!`, 'success');
254
+ this.updatePrivateAppUid(app, data, app.manifest.name);
255
+ }
256
+
257
+ resolve();
258
+ })
259
+ .catch((error) => {
260
+ if (error && (error.message || error.error_message)) {
261
+ log(self.config, error.message || error.error_message, 'error');
262
+ } else {
263
+ log(self.config, 'Something went wrong.!', 'error');
264
+ }
265
+
266
+ resolve();
267
+ });
268
+ });
269
+ };
270
+
271
+ /**
272
+ * @method updatePrivateAppUid
273
+ * @param {Object} app
274
+ * @param {Object} data
275
+ */
276
+ updatePrivateAppUid = (app, data, appName) => {
277
+ const self = this;
278
+ const allMarketplaceApps = readFileSync(
279
+ path.resolve(self.marketplaceAppFolderPath, self.marketplaceAppConfig.fileName),
280
+ );
281
+ const index = _.findIndex(allMarketplaceApps, { uid: app.uid, visibility: 'private' });
282
+
283
+ if (index > -1) {
284
+ allMarketplaceApps[index] = {
285
+ ...allMarketplaceApps[index],
286
+ title: data.name,
287
+ app_uid: data.uid,
288
+ old_title: allMarketplaceApps[index].title,
289
+ previous_data: [
290
+ ...(allMarketplaceApps[index].old_data || []),
291
+ { [`v${(allMarketplaceApps[index].old_data || []).length}`]: allMarketplaceApps[index] },
292
+ ],
293
+ };
294
+
295
+ // NOTE Update app name
296
+ allMarketplaceApps[index].manifest.name = appName;
297
+
298
+ writeFile(path.join(self.marketplaceAppFolderPath, self.marketplaceAppConfig.fileName), allMarketplaceApps);
299
+ }
300
+ };
301
+
302
+ /**
303
+ * @method installApps
304
+ * @param {Object} options
305
+ * @returns {Void}
306
+ */
307
+ installApps = (options) => {
308
+ const self = this;
309
+ const { app, installedExtensions, httpClient, nodeCrypto } = options;
310
+
311
+ return new Promise((resolve, reject) => {
312
+ httpClient
313
+ .post(`${self.developerHubBaseUrl}/apps/${app.app_uid}/install`, {
314
+ target_type: 'stack',
315
+ target_uid: self.config.target_stack,
316
+ })
317
+ .then(async ({ data: result }) => {
318
+ let updateParam;
319
+ const { title } = app;
320
+ const { data, error, message, error_code, error_message } = result;
321
+
322
+ if (error || error_code) {
323
+ // NOTE if already installed copy only config data
324
+ log(self.config, `${message || error_message} - ${title}`, 'success');
325
+ const ext = _.find(installedExtensions, { app_uid: app.app_uid });
326
+
327
+ if (ext) {
328
+ if (!_.isEmpty(app.configuration) || !_.isEmpty(app.server_configuration)) {
329
+ cliux.print(
330
+ `WARNING!!! The ${title} app already exists and it may have its own configuration. But the current app you install has its own configuration which is used internally to manage content.`,
331
+ { color: 'yellow' },
332
+ );
333
+
334
+ const configOption = self.config.forceMarketplaceAppsImport
335
+ ? 'Update it with the new configuration.'
336
+ : await cliux.inquire({
337
+ choices: [
338
+ 'Update it with the new configuration.',
339
+ 'Do not update the configuration (WARNING!!! If you do not update the configuration, there may be some issues with the content which you import).',
340
+ 'Exit',
341
+ ],
342
+ type: 'list',
343
+ name: 'value',
344
+ message: 'Choose the option to proceed',
345
+ });
346
+
347
+ if (configOption === 'Exit') {
348
+ process.exit();
349
+ } else if (configOption === 'Update it with the new configuration.') {
350
+ updateParam = {
351
+ app,
352
+ nodeCrypto,
353
+ httpClient,
354
+ data: { ...ext, installation_uid: ext.app_installation_uid },
355
+ };
356
+ }
357
+ }
358
+ } else {
359
+ if (!self.config.forceMarketplaceAppsImport) {
360
+ cliux.print(`WARNING!!! ${message || error_message}`, { color: 'yellow' });
361
+ const confirmation = await cliux.confirm(
362
+ chalk.yellow(
363
+ 'WARNING!!! The above error may have an impact if the failed app is referenced in entries/content type. Would you like to proceed? (y/n)',
364
+ ),
365
+ );
366
+
367
+ if (!confirmation) {
368
+ process.exit();
369
+ }
370
+ }
371
+ }
372
+ } else if (data) {
373
+ // NOTE new app installation
374
+ log(self.config, `${title} app installed successfully.!`, 'success');
375
+ updateParam = { data, app, nodeCrypto, httpClient };
376
+ }
377
+
378
+ if (updateParam) {
379
+ self.updateAppsConfig(updateParam).then(resolve).catch(reject);
380
+ } else {
381
+ resolve();
382
+ }
383
+ })
384
+ .catch((error) => {
385
+ if (error && (error.message || error.error_message)) {
386
+ log(self.config, error.message || error.error_message, 'error');
387
+ } else {
388
+ log(self.config, 'Something went wrong.!', 'error');
389
+ }
390
+
391
+ reject();
392
+ });
393
+ });
394
+ };
395
+
396
+ /**
397
+ * @method updateAppsConfig
398
+ * @param {Object<{ data, app, httpClient, nodeCrypto }>} param
399
+ * @returns {Promise<void>}
400
+ */
401
+ updateAppsConfig = ({ data, app, httpClient, nodeCrypto }) => {
402
+ const self = this;
403
+ return new Promise((resolve, reject) => {
404
+ const payload = {};
405
+ const { title, configuration, server_configuration } = app;
406
+
407
+ if (!_.isEmpty(configuration)) {
408
+ payload['configuration'] = nodeCrypto.decrypt(configuration);
409
+ }
410
+ if (!_.isEmpty(server_configuration)) {
411
+ payload['server_configuration'] = nodeCrypto.decrypt(server_configuration);
412
+ }
413
+
414
+ if (_.isEmpty(data) || _.isEmpty(payload) || !data.installation_uid) {
415
+ resolve();
416
+ } else {
417
+ httpClient
418
+ .put(`${this.developerHubBaseUrl}/installations/${data.installation_uid}`, payload)
419
+ .then(() => {
420
+ log(self.config, `${title} app config updated successfully.!`, 'success');
421
+ })
422
+ .then(resolve)
423
+ .catch((error) => {
424
+ if (error && (error.message || error.error_message)) {
425
+ log(self.config, error.message || error.error_message, 'error');
426
+ } else {
427
+ log(self.config, 'Something went wrong.!', 'error');
428
+ }
429
+
430
+ reject();
431
+ });
432
+ }
433
+ });
434
+ };
435
+
436
+ validateAppName = (name) => {
437
+ if (name.length < 3 || name.length > 20) {
438
+ return 'The app name should be within 3-20 characters long.';
439
+ }
440
+
441
+ return true;
442
+ };
443
+ };
@@ -9,113 +9,107 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const Promise = require('bluebird');
11
11
  const chalk = require('chalk');
12
- const {isEmpty} = require('lodash');
12
+ const { isEmpty, merge } = require('lodash');
13
13
 
14
14
  const helper = require('../util/fs');
15
+ const { formatError } = require('../util');
15
16
  const { addlogs } = require('../util/log');
16
- let config = require('../../config/default');
17
- let stack = require('../util/contentstack-management-sdk');
18
- let reqConcurrency = config.concurrency;
19
- let webhooksConfig = config.modules.webhooks;
17
+ const config = require('../../config/default');
18
+ const stack = require('../util/contentstack-management-sdk');
20
19
 
21
- let webhooksFolderPath;
22
- let webMapperPath;
23
- let webUidMapperPath;
24
- let webSuccessPath;
25
- let webFailsPath;
26
- let client;
20
+ module.exports = class ImportWebhooks {
21
+ config;
22
+ fails = [];
23
+ success = [];
24
+ webUidMapper = {};
25
+ webhooksConfig = config.modules.webhooks;
26
+ reqConcurrency = config.concurrency || config.fetchConcurrency;
27
27
 
28
- function importWebhooks() {
29
- this.fails = [];
30
- this.success = [];
31
- this.webUidMapper = {};
32
- }
28
+ constructor(credentialConfig) {
29
+ this.config = merge(config, credentialConfig);
30
+ }
31
+
32
+ start() {
33
+ addlogs(this.config, chalk.white('Migrating webhooks'), 'success');
34
+
35
+ const self = this;
36
+ const client = stack.Client(this.config);
37
+
38
+ let webMapperPath = path.resolve(this.config.data, 'mapper', 'webhooks');
39
+ let webFailsPath = path.resolve(this.config.data, 'mapper', 'webhooks', 'fails.json');
40
+ let webSuccessPath = path.resolve(this.config.data, 'mapper', 'webhooks', 'success.json');
41
+ let webUidMapperPath = path.resolve(this.config.data, 'mapper', 'webhooks', 'uid-mapping.json');
42
+
43
+ let webhooksFolderPath = path.resolve(this.config.data, this.webhooksConfig.dirName);
44
+ this.webhooks = helper.readFileSync(path.resolve(webhooksFolderPath, this.webhooksConfig.fileName));
33
45
 
34
- importWebhooks.prototype = {
35
- start: function (credentialConfig) {
36
- let self = this;
37
- config = credentialConfig;
38
- addlogs(config, chalk.white('Migrating webhooks'), 'success');
39
- client = stack.Client(config);
40
- webhooksFolderPath = path.resolve(config.data, webhooksConfig.dirName);
41
- webMapperPath = path.resolve(config.data, 'mapper', 'webhooks');
42
- webUidMapperPath = path.resolve(config.data, 'mapper', 'webhooks', 'uid-mapping.json');
43
- webSuccessPath = path.resolve(config.data, 'mapper', 'webhooks', 'success.json');
44
- webFailsPath = path.resolve(config.data, 'mapper', 'webhooks', 'fails.json');
45
- self.webhooks = helper.readFile(path.resolve(webhooksFolderPath, webhooksConfig.fileName));
46
46
  if (fs.existsSync(webUidMapperPath)) {
47
- self.webUidMapper = helper.readFile(webUidMapperPath);
47
+ self.webUidMapper = helper.readFileSync(webUidMapperPath);
48
48
  self.webUidMapper = self.webUidMapper || {};
49
49
  }
50
+
50
51
  mkdirp.sync(webMapperPath);
51
52
 
52
53
  return new Promise(function (resolve, reject) {
53
54
  if (self.webhooks == undefined || isEmpty(self.webhooks)) {
54
- addlogs(config, chalk.white('No Webhooks Found'), 'success');
55
+ addlogs(self.config, chalk.white('No Webhooks Found'), 'success');
55
56
  return resolve({ empty: true });
56
57
  }
58
+
57
59
  let webUids = Object.keys(self.webhooks);
58
60
  return Promise.map(
59
61
  webUids,
60
62
  function (webUid) {
61
63
  let web = self.webhooks[webUid];
62
- if (config.importWebhookStatus !== 'current' || config.importWebhookStatus === 'disable') {
64
+ if (self.config.importWebhookStatus !== 'current' || self.config.importWebhookStatus === 'disable') {
63
65
  web.disabled = true;
64
66
  }
65
67
 
66
68
  if (!self.webUidMapper.hasOwnProperty(webUid)) {
67
- let requestOption = {
68
- json: {
69
- webhook: web,
70
- },
71
- };
69
+ let requestOption = { json: { webhook: web } };
72
70
 
73
71
  return client
74
- .stack({ api_key: config.target_stack, management_token: config.management_token })
72
+ .stack({ api_key: self.config.target_stack, management_token: self.config.management_token })
75
73
  .webhook()
76
74
  .create(requestOption.json)
77
75
  .then(function (response) {
78
76
  self.success.push(response);
79
77
  self.webUidMapper[webUid] = response.uid;
80
- helper.writeFile(webUidMapperPath, self.webUidMapper);
78
+ helper.writeFileSync(webUidMapperPath, self.webUidMapper);
81
79
  })
82
80
  .catch(function (err) {
83
81
  let error = JSON.parse(err.message);
84
82
  self.fails.push(web);
85
83
  addlogs(
86
- config,
87
- chalk.red("Webhooks: '" + web.name + "' failed to be import\n" + JSON.stringify(error)),
84
+ self.config,
85
+ chalk.red("Webhooks: '" + web.name + "' failed to be import\n" + formatError(error)),
88
86
  'error',
89
87
  );
90
88
  });
91
89
  } else {
92
90
  // the webhooks has already been created
93
91
  addlogs(
94
- config,
92
+ self.config,
95
93
  chalk.white("The Webhooks: '" + web.name + "' already exists. Skipping it to avoid duplicates!"),
96
94
  'success',
97
95
  );
98
96
  }
99
97
  // import 2 webhooks at a time
100
98
  },
101
- {
102
- concurrency: reqConcurrency,
103
- },
99
+ { concurrency: self.reqConcurrency },
104
100
  )
105
101
  .then(function () {
106
102
  // webhooks have imported successfully
107
- helper.writeFile(webSuccessPath, self.success);
108
- addlogs(config, chalk.green('Webhooks have been imported successfully!'), 'success');
103
+ helper.writeFileSync(webSuccessPath, self.success);
104
+ addlogs(self.config, chalk.green('Webhooks have been imported successfully!'), 'success');
109
105
  return resolve();
110
106
  })
111
107
  .catch(function (error) {
112
108
  // error while importing environments
113
- helper.writeFile(webFailsPath, self.fails);
114
- addlogs(config, chalk.red('Webhooks import failed'), 'error');
109
+ helper.writeFileSync(webFailsPath, self.fails);
110
+ addlogs(self.config, chalk.red('Webhooks import failed'), 'error');
115
111
  return reject(error);
116
112
  });
117
113
  });
118
- },
114
+ }
119
115
  };
120
-
121
- module.exports = new importWebhooks();