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