@contentstack/cli-cm-import 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,547 +8,522 @@ const _ = require('lodash');
8
8
  const path = require('path');
9
9
  const chalk = require('chalk');
10
10
  const mkdirp = require('mkdirp');
11
- const { cliux, HttpClient, NodeCrypto } = require('@contentstack/cli-utilities');
11
+ const contentstack = require('@contentstack/management');
12
+ const { cliux, HttpClient, NodeCrypto, managementSDKClient } = require('@contentstack/cli-utilities');
12
13
 
14
+ const { formatError } = require('../util');
13
15
  let config = require('../../config/default');
14
16
  const { addlogs: log } = require('../util/log');
15
17
  const { readFileSync, writeFile } = require('../util/fs');
16
- const sdk = require('../util/contentstack-management-sdk');
17
- const { getDeveloperHubUrl, getInstalledExtensions } = require('../util/marketplace-app-helper');
18
+ const { getDeveloperHubUrl, getAllStackSpecificApps } = require('../util/marketplace-app-helper');
18
19
 
19
20
  module.exports = class ImportMarketplaceApps {
20
21
  client;
22
+ httpClient;
23
+ appOrginalName;
24
+ appUidMapping = {};
25
+ appNameMapping = {};
21
26
  marketplaceApps = [];
22
- marketplaceAppsUid = [];
27
+ installationUidMapping = {};
23
28
  developerHubBaseUrl = null;
24
29
  marketplaceAppFolderPath = '';
25
30
  marketplaceAppConfig = config.modules.marketplace_apps;
26
31
 
27
- constructor(credentialConfig) {
28
- this.config = _.merge(config, credentialConfig);
32
+ constructor(importConfig, stackAPIClient) {
33
+ this.config = _.merge(config, importConfig);
34
+ this.stackAPIClient = stackAPIClient;
29
35
  }
30
36
 
31
37
  async start() {
32
- this.client = sdk.Client(this.config);
33
- this.developerHubBaseUrl = this.config.developerHubBaseUrl || (await getDeveloperHubUrl());
38
+ this.developerHubBaseUrl = this.config.developerHubBaseUrl || (await getDeveloperHubUrl(this.config));
39
+ this.client = contentstack.client({ authtoken: this.config.auth_token, endpoint: this.developerHubBaseUrl });
40
+ this.mapperDirPath = path.resolve(this.config.data, 'mapper', 'marketplace_apps');
41
+ this.uidMapperPath = path.join(this.mapperDirPath, 'uid-mapping.json');
34
42
  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',
43
+ this.marketplaceApps = readFileSync(
44
+ path.resolve(this.marketplaceAppFolderPath, this.marketplaceAppConfig.fileName),
38
45
  );
39
- this.marketplaceAppsUid = _.map(this.marketplaceApps, 'uid');
40
46
 
41
- if (!this.config.auth_token && !_.isEmpty(this.marketplaceApps)) {
47
+ if (_.isEmpty(this.marketplaceApps)) {
48
+ return Promise.resolve();
49
+ } else if (!this.config.auth_token) {
42
50
  cliux.print(
43
- 'WARNING!!! To import Marketplace apps, you must be logged in. Please check csdx auth:login --help to log in',
51
+ '\nWARNING!!! To import Marketplace apps, you must be logged in. Please check csdx auth:login --help to log in\n',
44
52
  { color: 'yellow' },
45
53
  );
46
54
  return Promise.resolve();
47
- } else if (_.isEmpty(this.marketplaceApps)) {
48
- return Promise.resolve();
49
55
  }
50
56
 
51
57
  await this.getOrgUid();
52
- return this.handleInstallationProcess();
58
+
59
+ this.httpClient = new HttpClient({
60
+ headers: {
61
+ authtoken: this.config.auth_token,
62
+ organization_uid: this.config.org_uid,
63
+ },
64
+ });
65
+
66
+ if (!fs.existsSync(this.mapperDirPath)) {
67
+ mkdirp.sync(this.mapperDirPath);
68
+ }
69
+
70
+ return this.startInstallation();
53
71
  }
54
72
 
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 })
73
+ async getOrgUid() {
74
+ if (this.config.auth_token) {
75
+ const tempAPIClient = await managementSDKClient({ host: this.config.host });
76
+ const tempStackData = await tempAPIClient
77
+ .stack({ api_key: this.config.target_stack })
65
78
  .fetch()
66
79
  .catch((error) => {
67
80
  console.log(error);
68
- log(self.config, 'Starting marketplace app installation', 'success');
69
81
  });
70
82
 
71
- if (stack && stack.org_uid) {
72
- self.config.org_uid = stack.org_uid;
83
+ if (tempStackData && tempStackData.org_uid) {
84
+ this.config.org_uid = tempStackData.org_uid;
73
85
  }
74
86
  }
75
- };
76
-
77
- async getEncryptionKeyAndValidate(defaultValue, retry = 1) {
78
- const appConfig =
79
- _.find(this.marketplaceApps, 'configuration') ||
80
- _.find(this.marketplaceApps, 'server_configuration.configuration');
81
-
82
- if (appConfig) {
83
- const encryptionKey = await cliux.inquire({
84
- type: 'input',
85
- name: 'name',
86
- default: defaultValue,
87
- validate: (key) => {
88
- if (!key) return "Encryption key can't be empty.";
89
-
90
- return true;
91
- },
92
- message: 'Enter marketplace app configurations encryption key',
93
- });
94
-
95
- try {
96
- const nodeCrypto = new NodeCrypto({ encryptionKey });
97
- nodeCrypto.decrypt(appConfig.configuration || appConfig.server_configuration);
98
- } catch (error) {
99
- if (retry < this.config.getEncryptionKeyMaxRetry && error.code === 'ERR_OSSL_EVP_BAD_DECRYPT') {
100
- cliux.print('Provided encryption key is not valid or your data might be corrupted.!', { color: 'red' });
101
- // NOTE max retry limit is 3
102
- return this.getEncryptionKeyAndValidate(encryptionKey, retry + 1);
103
- } else {
104
- cliux.print('Maximum retry limit exceeded. Closing the process, please try again.!', { color: 'red' });
105
- process.exit(1);
106
- }
107
- }
87
+ }
108
88
 
109
- return encryptionKey;
89
+ async getAndValidateEncryptionKey(defaultValue, retry = 1) {
90
+ let appConfig = _.find(
91
+ this.marketplaceApps,
92
+ ({ configuration, server_configuration }) => !_.isEmpty(configuration) || !_.isEmpty(server_configuration),
93
+ );
94
+
95
+ if (!appConfig) {
96
+ return defaultValue;
110
97
  }
111
98
 
112
- return defaultValue;
99
+ const encryptionKey = await cliux.inquire({
100
+ type: 'input',
101
+ name: 'name',
102
+ default: defaultValue,
103
+ validate: (key) => {
104
+ if (!key) return "Encryption key can't be empty.";
105
+
106
+ return true;
107
+ },
108
+ message: 'Enter marketplace app configurations encryption key',
109
+ });
110
+
111
+ try {
112
+ appConfig = !_.isEmpty(appConfig.configuration) ? appConfig.configuration : appConfig.server_configuration;
113
+ this.nodeCrypto = new NodeCrypto({ encryptionKey });
114
+ this.nodeCrypto.decrypt(appConfig);
115
+ } catch (error) {
116
+ if (retry < this.config.getEncryptionKeyMaxRetry && error.code === 'ERR_OSSL_EVP_BAD_DECRYPT') {
117
+ cliux.print(
118
+ `Provided encryption key is not valid or your data might be corrupted.! attempt(${retry}/${this.config.getEncryptionKeyMaxRetry})`,
119
+ { color: 'red' },
120
+ );
121
+ // NOTE max retry limit is 3
122
+ return this.getAndValidateEncryptionKey(encryptionKey, retry + 1);
123
+ } else {
124
+ cliux.print(
125
+ `Maximum retry limit exceeded. Closing the process, please try again.! attempt(${retry}/${this.config.getEncryptionKeyMaxRetry})`,
126
+ { color: 'red' },
127
+ );
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ return encryptionKey;
113
133
  }
114
134
 
115
135
  /**
116
- * @method handleInstallationProcess
136
+ * @method startInstallation
117
137
  * @returns {Promise<void>}
118
138
  */
119
- handleInstallationProcess = async () => {
120
- const self = this;
139
+ async startInstallation() {
121
140
  const cryptoArgs = {};
122
- const headers = {
123
- authtoken: self.config.auth_token,
124
- organization_uid: self.config.org_uid,
125
- };
126
141
 
127
- if (self.config.marketplaceAppEncryptionKey) {
128
- cryptoArgs['encryptionKey'] = self.config.marketplaceAppEncryptionKey;
142
+ if (this.config.marketplaceAppEncryptionKey) {
143
+ cryptoArgs['encryptionKey'] = this.config.marketplaceAppEncryptionKey;
129
144
  }
130
145
 
131
- if (self.config.forceStopMarketplaceAppsPrompt) {
132
- cryptoArgs['encryptionKey'] = self.config.marketplaceAppEncryptionKey;
146
+ if (this.config.forceStopMarketplaceAppsPrompt) {
147
+ cryptoArgs['encryptionKey'] = this.config.marketplaceAppEncryptionKey;
148
+ this.nodeCrypto = new NodeCrypto(cryptoArgs);
133
149
  } else {
134
- cryptoArgs['encryptionKey'] = await self.getEncryptionKeyAndValidate(self.config.marketplaceAppEncryptionKey);
150
+ await this.getAndValidateEncryptionKey(this.config.marketplaceAppEncryptionKey);
135
151
  }
136
152
 
137
- const httpClient = new HttpClient().headers(headers);
138
- const nodeCrypto = new NodeCrypto(cryptoArgs);
139
-
140
153
  // NOTE install all private apps which is not available for stack.
141
- await this.handleAllPrivateAppsCreationProcess({ httpClient });
142
- const installedExtensions = await getInstalledExtensions(self.config);
143
-
144
- // NOTE after private app installation, refetch marketplace apps from file
145
- const marketplaceAppsFromFile = readFileSync(
146
- path.resolve(this.marketplaceAppFolderPath, self.marketplaceAppConfig.fileName),
147
- );
148
- this.marketplaceApps = _.filter(marketplaceAppsFromFile, ({ uid }) => _.includes(this.marketplaceAppsUid, uid));
154
+ await this.handleAllPrivateAppsCreationProcess();
155
+ const installedApps = await getAllStackSpecificApps(this.developerHubBaseUrl, this.httpClient, this.config);
149
156
 
150
- log(self.config, 'Starting marketplace app installation', 'success');
157
+ log(this.config, 'Starting marketplace app installation', 'success');
151
158
 
152
- for (let app of self.marketplaceApps) {
153
- await self.installApps({ app, installedExtensions, httpClient, nodeCrypto });
159
+ for (let app of this.marketplaceApps) {
160
+ await this.installApps(app, installedApps);
154
161
  }
155
162
 
156
- // NOTE get all the extension again after all apps installed (To manage uid mapping in content type, entries)
157
- const extensions = await getInstalledExtensions(self.config);
158
- const mapperFolderPath = path.join(self.config.data, 'mapper', 'marketplace_apps');
163
+ const uidMapper = await this.generateUidMapper();
164
+ await writeFile(this.uidMapperPath, {
165
+ app_uid: this.appUidMapping,
166
+ extension_uid: uidMapper || {},
167
+ installation_uid: this.installationUidMapping,
168
+ });
169
+ }
159
170
 
160
- if (!fs.existsSync(mapperFolderPath)) {
161
- mkdirp.sync(mapperFolderPath);
162
- }
171
+ async generateUidMapper() {
172
+ const listOfNewMeta = [];
173
+ const listOfOldMeta = [];
174
+ const extensionUidMapp = {};
175
+ const allInstalledApps = await getAllStackSpecificApps(this.developerHubBaseUrl, this.httpClient, this.config);
163
176
 
164
- const appUidMapperPath = path.join(mapperFolderPath, 'marketplace-apps.json');
165
- const installedExt = _.map(extensions, (row) =>
166
- _.pick(row, ['uid', 'title', 'type', 'app_uid', 'app_installation_uid']),
167
- );
177
+ for (const app of this.marketplaceApps) {
178
+ listOfOldMeta.push(..._.map(app.ui_location && app.ui_location.locations, 'meta').flat());
179
+ }
180
+ for (const app of allInstalledApps) {
181
+ listOfNewMeta.push(..._.map(app.ui_location && app.ui_location.locations, 'meta').flat());
182
+ }
183
+ for (const { extension_uid, name, path } of _.filter(listOfOldMeta, 'name')) {
184
+ const meta =
185
+ _.find(listOfNewMeta, { name, path }) || _.find(listOfNewMeta, { name: this.appNameMapping[name], path });
168
186
 
169
- writeFile(appUidMapperPath, installedExt);
187
+ if (meta) {
188
+ extensionUidMapp[extension_uid] = meta.extension_uid;
189
+ }
190
+ }
170
191
 
171
- return Promise.resolve();
172
- };
192
+ return extensionUidMapp;
193
+ }
173
194
 
174
195
  /**
175
196
  * @method handleAllPrivateAppsCreationProcess
176
197
  * @param {Object} options
177
198
  * @returns {Promise<void>}
178
199
  */
179
- handleAllPrivateAppsCreationProcess = async (options) => {
180
- const self = this;
181
- const { httpClient } = options;
182
- const listOfExportedPrivateApps = _.filter(self.marketplaceApps, { visibility: 'private' });
200
+ async handleAllPrivateAppsCreationProcess() {
201
+ const privateApps = _.filter(this.marketplaceApps, { manifest: { visibility: 'private' } });
183
202
 
184
- if (_.isEmpty(listOfExportedPrivateApps)) {
203
+ if (_.isEmpty(privateApps)) {
185
204
  return Promise.resolve();
186
205
  }
187
206
 
188
- // NOTE get list of developer-hub installed apps (private)
189
- const installedDeveloperHubApps =
190
- (await httpClient
191
- .get(`${this.developerHubBaseUrl}/apps`)
192
- .then(({ data: { data } }) => data)
193
- .catch((err) => {
194
- console.log(err);
195
- })) || [];
196
- const listOfNotInstalledPrivateApps = _.filter(
197
- listOfExportedPrivateApps,
198
- (app) => !_.includes(_.map(installedDeveloperHubApps, 'uid'), app.app_uid),
199
- );
207
+ await this.getConfirmationToCreateApps(privateApps);
208
+
209
+ log(this.config, 'Starting developer hub private apps re-creation', 'success');
210
+
211
+ for (let app of privateApps) {
212
+ // NOTE keys can be passed to install new app in the developer hub
213
+ app.manifest = _.pick(app.manifest, [
214
+ 'uid',
215
+ 'name',
216
+ 'description',
217
+ 'icon',
218
+ 'target_type',
219
+ 'ui_location',
220
+ 'webhook',
221
+ 'oauth',
222
+ ]);
223
+ this.appOrginalName = app.manifest.name;
224
+ await this.createPrivateApps(app.manifest);
225
+ }
200
226
 
201
- if (!_.isEmpty(listOfNotInstalledPrivateApps) && !self.config.forceStopMarketplaceAppsPrompt) {
202
- const confirmation = await cliux.confirm(
203
- chalk.yellow(
204
- `WARNING!!! The listed apps are private apps that are not available in the destination stack: \n\n${_.map(
205
- listOfNotInstalledPrivateApps,
206
- ({ manifest: { name } }, index) => `${String(index + 1)}) ${name}`,
207
- ).join('\n')}\n\nWould you like to re-create the private app and then proceed with the installation? (y/n)`,
208
- ),
209
- );
227
+ this.appOrginalName = undefined;
228
+ }
210
229
 
211
- if (!confirmation) {
212
- const continueProcess = await cliux.confirm(
230
+ async getConfirmationToCreateApps(privateApps) {
231
+ if (!this.config.forceStopMarketplaceAppsPrompt) {
232
+ if (
233
+ !(await cliux.confirm(
213
234
  chalk.yellow(
214
- `WARNING!!! Canceling the app re-creation may break the content type and entry import. Would you like to proceed? (y/n)`,
235
+ `WARNING!!! The listed apps are private apps that are not available in the destination stack: \n\n${_.map(
236
+ privateApps,
237
+ ({ manifest: { name } }, index) => `${String(index + 1)}) ${name}`,
238
+ ).join('\n')}\n\nWould you like to re-create the private app and then proceed with the installation? (y/n)`,
215
239
  ),
216
- );
240
+ ))
241
+ ) {
242
+ if (
243
+ await cliux.confirm(
244
+ chalk.yellow(
245
+ `\nWARNING!!! Canceling the app re-creation may break the content type and entry import. Would you like to proceed without re-create the private app? (y/n)`,
246
+ ),
247
+ )
248
+ ) {
249
+ return Promise.resolve(true);
250
+ }
217
251
 
218
- if (continueProcess) {
219
- return Promise.resolve();
220
- } else {
252
+ if (
253
+ !(await cliux.confirm(
254
+ chalk.yellow('\nWould you like to re-create the private app and then proceed with the installation? (y/n)'),
255
+ ))
256
+ ) {
221
257
  process.exit();
222
258
  }
223
259
  }
224
260
  }
261
+ }
225
262
 
226
- log(self.config, 'Starting developer hub private apps re-creation', 'success');
263
+ async createPrivateApps(app, uidCleaned = false, appSuffix = 1) {
264
+ let locations = app.ui_location && app.ui_location.locations;
227
265
 
228
- for (let app of listOfNotInstalledPrivateApps) {
229
- await self.createAllPrivateAppsInDeveloperHub({ app, httpClient });
266
+ if (!uidCleaned && !_.isEmpty(locations)) {
267
+ app.ui_location.locations = this.uodateManifestUILocations(locations, 'uid');
268
+ } else if (uidCleaned && !_.isEmpty(locations)) {
269
+ app.ui_location.locations = this.uodateManifestUILocations(locations, 'name', appSuffix);
230
270
  }
231
271
 
232
- return Promise.resolve();
233
- };
272
+ if (app.name > 20) {
273
+ app.name = app.name.slice(0, 20);
274
+ }
234
275
 
235
- /**
236
- * @method removeUidFromManifestUILocations
237
- * @param {Array<Object>} locations
238
- * @returns {Array<Object>}
239
- */
240
- removeUidFromManifestUILocations = (locations) => {
241
- return _.map(locations, (location) => {
242
- if (location.meta) {
243
- location.meta = _.map(location.meta, (meta) => _.omit(meta, ['uid']));
244
- }
276
+ const response = await this.client
277
+ .organization(this.config.org_uid)
278
+ .app()
279
+ .create(_.omit(app, ['uid']))
280
+ .catch((error) => error);
245
281
 
246
- return location;
247
- });
248
- };
282
+ return this.appCreationCallback(app, response, appSuffix);
283
+ }
249
284
 
250
- /**
251
- * @method updateNameInManifestUILocations
252
- * @param {Array<Object>} locations
253
- * @returns {Array<Object>}
254
- */
255
- updateNameInManifestUILocations = (locations, appSuffix = 1) => {
256
- return _.map(locations, (location) => {
257
- if (location.meta) {
258
- location.meta = _.map(location.meta, (meta) => {
259
- meta.name = `${_.first(_.split(meta.name, '◈'))}◈${appSuffix}`;
285
+ async appCreationCallback(app, response, appSuffix) {
286
+ const { statusText, message } = response || {};
260
287
 
261
- return meta;
262
- });
263
- }
288
+ if (message) {
289
+ if (_.toLower(statusText) === 'conflict') {
290
+ return this.handleNameConflict(app, appSuffix);
291
+ } else {
292
+ log(this.config, formatError(message), 'error');
264
293
 
265
- return location;
266
- });
267
- };
294
+ if (this.config.forceStopMarketplaceAppsPrompt) return resolve();
268
295
 
269
- getAppName = (name, appSuffix = 1) => {
270
- if (name.length >= 19) name = name.slice(0, 18);
296
+ if (
297
+ await cliux.confirm(
298
+ chalk.yellow(
299
+ '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)',
300
+ ),
301
+ )
302
+ ) {
303
+ resolve();
304
+ } else {
305
+ process.exit();
306
+ }
307
+ }
308
+ } else if (response.uid) {
309
+ // NOTE new app installation
310
+ log(this.config, `${response.name} app created successfully.!`, 'success');
311
+ this.appUidMapping[app.uid] = response.uid;
312
+ this.appNameMapping[this.appOrginalName] = response.name;
313
+ }
314
+ }
271
315
 
272
- name = `${_.first(_.split(name, '◈'))}◈${appSuffix}`;
316
+ async handleNameConflict(app, appSuffix) {
317
+ const appName = this.config.forceStopMarketplaceAppsPrompt
318
+ ? this.getAppName(app.name, appSuffix)
319
+ : await cliux.inquire({
320
+ type: 'input',
321
+ name: 'name',
322
+ validate: this.validateAppName,
323
+ default: this.getAppName(app.name, appSuffix),
324
+ message: `${app.name} app already exist. Enter a new name to create an app.?`,
325
+ });
326
+ app.name = appName;
273
327
 
274
- return name;
275
- };
328
+ return this.createPrivateApps(app, true, appSuffix + 1);
329
+ }
276
330
 
277
- /**
278
- * @method createAllPrivateAppsInDeveloperHub
279
- * @param {Object} options
280
- * @returns {Promise<void>}
281
- */
282
- createAllPrivateAppsInDeveloperHub = async (options, uidCleaned = false, appSuffix = 1, isRecursive = false) => {
283
- const self = this;
284
- const { app, httpClient } = options;
285
-
286
- return new Promise((resolve) => {
287
- if (!uidCleaned && app.manifest.ui_location && !_.isEmpty(app.manifest.ui_location.locations)) {
288
- app.manifest.ui_location.locations = this.removeUidFromManifestUILocations(app.manifest.ui_location.locations);
289
- } else if (isRecursive && app.manifest.ui_location && !_.isEmpty(app.manifest.ui_location.locations)) {
290
- app.manifest.ui_location.locations = this.updateNameInManifestUILocations(
291
- app.manifest.ui_location.locations,
292
- appSuffix - 1 || 1,
293
- isRecursive,
294
- );
295
- }
331
+ uodateManifestUILocations(locations, type = 'uid', appSuffix = 1) {
332
+ switch (type) {
333
+ case 'uid':
334
+ return _.map(locations, (location) => {
335
+ if (location.meta) {
336
+ location.meta = _.map(location.meta, (meta) => _.omit(meta, ['uid']));
337
+ }
296
338
 
297
- if (app.manifest.name > 20) {
298
- app.manifest.name = app.manifest.name.slice(0, 20);
299
- }
339
+ return location;
340
+ });
341
+ case 'name':
342
+ return _.map(locations, (location) => {
343
+ if (location.meta) {
344
+ location.meta = _.map(location.meta, (meta) => {
345
+ if (meta.name) {
346
+ const name = `${_.first(_.split(meta.name, '◈'))}◈${appSuffix}`;
347
+
348
+ if (!this.appNameMapping[this.appOrginalName]) {
349
+ this.appNameMapping[this.appOrginalName] = name;
350
+ }
300
351
 
301
- httpClient
302
- .post(`${this.developerHubBaseUrl}/apps`, app.manifest)
303
- .then(async ({ data: result }) => {
304
- const { name } = app.manifest;
305
- const { data, error, message } = result || {};
306
-
307
- if (error) {
308
- log(self.config, message, 'error');
309
-
310
- if (_.toLower(error) === 'conflict') {
311
- const appName = self.config.forceStopMarketplaceAppsPrompt
312
- ? self.getAppName(app.manifest.name, appSuffix)
313
- : await cliux.inquire({
314
- type: 'input',
315
- name: 'name',
316
- default: self.getAppName(app.manifest.name, appSuffix),
317
- validate: this.validateAppName,
318
- message: `${message}. Enter a new name to create an app.?`,
319
- });
320
- app.manifest.name = appName;
321
-
322
- await self
323
- .createAllPrivateAppsInDeveloperHub({ app, httpClient }, true, appSuffix + 1, true)
324
- .then(resolve)
325
- .catch(resolve);
326
- } else {
327
- if (self.config.forceStopMarketplaceAppsPrompt) return resolve();
328
-
329
- const confirmation = await cliux.confirm(
330
- chalk.yellow(
331
- '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)',
332
- ),
333
- );
334
-
335
- if (confirmation) {
336
- resolve();
337
- } else {
338
- process.exit();
352
+ meta.name = name;
339
353
  }
340
- }
341
- } else if (data) {
342
- // NOTE new app installation
343
- log(self.config, `${name} app created successfully.!`, 'success');
344
- this.updatePrivateAppUid(app, data, app.manifest);
345
- }
346
354
 
347
- resolve();
348
- })
349
- .catch((error) => {
350
- if (error && (error.message || error.error_message)) {
351
- log(self.config, error.message || error.error_message, 'error');
352
- } else {
353
- log(self.config, 'Something went wrong.!', 'error');
355
+ return meta;
356
+ });
354
357
  }
355
358
 
356
- resolve();
359
+ return location;
357
360
  });
358
- });
359
- };
360
-
361
- /**
362
- * @method updatePrivateAppUid
363
- * @param {Object} app
364
- * @param {Object} data
365
- */
366
- updatePrivateAppUid = (app, data, manifest) => {
367
- const self = this;
368
- const allMarketplaceApps = readFileSync(
369
- path.resolve(self.marketplaceAppFolderPath, self.marketplaceAppConfig.fileName),
370
- );
371
- const index = _.findIndex(allMarketplaceApps, { uid: app.uid, visibility: 'private' });
372
-
373
- if (index > -1) {
374
- allMarketplaceApps[index] = {
375
- ...allMarketplaceApps[index],
376
- manifest,
377
- title: data.name,
378
- app_uid: data.uid,
379
- old_title: allMarketplaceApps[index].title,
380
- previous_data: [
381
- ...(allMarketplaceApps[index].old_data || []),
382
- { [`v${(allMarketplaceApps[index].old_data || []).length}`]: allMarketplaceApps[index] },
383
- ],
384
- };
385
-
386
- writeFile(path.join(self.marketplaceAppFolderPath, self.marketplaceAppConfig.fileName), allMarketplaceApps);
387
361
  }
388
- };
362
+ }
363
+
364
+ getAppName(name, appSuffix = 1) {
365
+ if (name.length >= 19) name = name.slice(0, 18);
366
+
367
+ name = `${_.first(_.split(name, '◈'))}◈${appSuffix}`;
368
+
369
+ return name;
370
+ }
389
371
 
390
372
  /**
391
373
  * @method installApps
392
374
  * @param {Object} options
393
375
  * @returns {Void}
394
376
  */
395
- installApps = (options) => {
396
- const self = this;
397
- const { app, installedExtensions, httpClient, nodeCrypto } = options;
398
-
399
- return new Promise((resolve, reject) => {
400
- httpClient
401
- .post(`${self.developerHubBaseUrl}/apps/${app.app_uid}/install`, {
402
- target_type: 'stack',
403
- target_uid: self.config.target_stack,
404
- })
405
- .then(async ({ data: result }) => {
406
- let updateParam;
407
- const { title } = app;
408
- const { data, error, message, error_code, error_message } = result;
409
-
410
- if (error || error_code) {
411
- // NOTE if already installed copy only config data
412
- log(self.config, `${message || error_message} - ${title}`, 'success');
413
- const ext = _.find(installedExtensions, { app_uid: app.app_uid });
414
-
415
- if (ext) {
416
- if (!_.isEmpty(app.configuration) || !_.isEmpty(app.server_configuration)) {
417
- cliux.print(
418
- `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.`,
419
- { color: 'yellow' },
420
- );
421
-
422
- const configOption = self.config.forceStopMarketplaceAppsPrompt
423
- ? 'Update it with the new configuration.'
424
- : await cliux.inquire({
425
- choices: [
426
- 'Update it with the new configuration.',
427
- 'Do not update the configuration (WARNING!!! If you do not update the configuration, there may be some issues with the content which you import).',
428
- 'Exit',
429
- ],
430
- type: 'list',
431
- name: 'value',
432
- message: 'Choose the option to proceed',
433
- });
434
-
435
- if (configOption === 'Exit') {
436
- process.exit();
437
- } else if (configOption === 'Update it with the new configuration.') {
438
- updateParam = {
439
- app,
440
- nodeCrypto,
441
- httpClient,
442
- data: { ...ext, installation_uid: ext.app_installation_uid },
443
- };
444
- }
445
- }
446
- } else {
447
- if (!self.config.forceStopMarketplaceAppsPrompt) {
448
- cliux.print(`WARNING!!! ${message || error_message}`, { color: 'yellow' });
449
- const confirmation = await cliux.confirm(
450
- chalk.yellow(
451
- '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)',
452
- ),
453
- );
454
-
455
- if (!confirmation) {
456
- process.exit();
457
- }
458
- }
459
- }
460
- } else if (data) {
461
- // NOTE new app installation
462
- log(self.config, `${title} app installed successfully.!`, 'success');
463
- updateParam = { data, app, nodeCrypto, httpClient };
464
- }
377
+ async installApps(app, installedApps) {
378
+ let updateParam;
379
+ let installation;
380
+ const { configuration, server_configuration } = app;
381
+ const currentStackApp = _.find(installedApps, { manifest: { uid: app.manifest.uid } });
382
+
383
+ if (!currentStackApp) {
384
+ // NOTE install new app
385
+ installation = await this.client
386
+ .organization(this.config.org_uid)
387
+ .app(this.appUidMapping[app.manifest.uid] || app.manifest.uid)
388
+ .install({ targetUid: this.config.target_stack, targetType: 'stack' })
389
+ .catch((error) => error);
390
+
391
+ if (installation.installation_uid) {
392
+ log(this.config, `${app.manifest.name} app installed successfully.!`, 'success');
393
+ await this.makeRedirectUrlCall(installation, app.manifest.name);
394
+ this.installationUidMapping[app.uid] = installation.installation_uid;
395
+ updateParam = { manifest: app.manifest, ...installation, configuration, server_configuration };
396
+ } else if (installation.message) {
397
+ log(this.config, formatError(installation.message), 'success');
398
+ await this.confirmToCloseProcess(installation);
399
+ }
400
+ } else if (!_.isEmpty(configuration) || !_.isEmpty(server_configuration)) {
401
+ log(this.config, `${app.manifest.name} is already installed`, 'success');
402
+ updateParam = await this.ifAppAlreadyExist(app, currentStackApp);
403
+ }
404
+
405
+ if (!this.appUidMapping[app.manifest.uid]) {
406
+ this.appUidMapping[app.manifest.uid] = currentStackApp ? currentStackApp.manifest.uid : app.manifest.uid;
407
+ }
408
+
409
+ // NOTE update configurations
410
+ if (updateParam && (!_.isEmpty(updateParam.configuration) || !_.isEmpty(updateParam.server_configuration))) {
411
+ await this.updateAppsConfig(updateParam);
412
+ }
413
+ }
465
414
 
466
- if (updateParam) {
467
- self.updateAppsConfig(updateParam).then(resolve).catch(reject);
415
+ async makeRedirectUrlCall(response, appName) {
416
+ if (response.redirect_url) {
417
+ log(this.config, `${appName} - OAuth api call started.!`, 'info');
418
+ await new HttpClient({ maxRedirects: 20, maxBodyLength: Infinity })
419
+ .get(response.redirect_url)
420
+ .then(async ({ response }) => {
421
+ if (_.includes([501, 403], response.status)) {
422
+ log(this.config, `${appName} - ${response.statusText}, OAuth api call failed.!`, 'error');
423
+ log(this.config, formatError(response), 'error');
424
+ await this.confirmToCloseProcess({ message: response.data });
468
425
  } else {
469
- resolve();
426
+ log(this.config, `${appName} - OAuth api call completed.!`, 'success');
470
427
  }
471
428
  })
472
429
  .catch((error) => {
473
- if (error && (error.message || error.error_message)) {
474
- log(self.config, error.message || error.error_message, 'error');
475
- } else {
476
- log(self.config, 'Something went wrong.!', 'error');
430
+ if (_.includes([501, 403], error.status)) {
431
+ log(this.config, formatError(error), 'error');
477
432
  }
478
-
479
- reject();
480
433
  });
481
- });
482
- };
483
-
484
- updateConfigData(config) {
485
- const configKeyMapper = {};
486
- const serverConfigKeyMapper = {
487
- cmsApiKey: this.config.target_stack,
488
- };
489
-
490
- if (!_.isEmpty(config.configuration) && !_.isEmpty(configKeyMapper)) {
491
- _.forEach(_.keys(configKeyMapper), (key) => {
492
- config.configuration[key] = configKeyMapper[key];
493
- });
494
434
  }
435
+ }
436
+
437
+ async ifAppAlreadyExist(app, currentStackApp) {
438
+ let updateParam;
439
+ const {
440
+ manifest: { name },
441
+ configuration,
442
+ server_configuration,
443
+ } = app;
444
+
445
+ if (!_.isEmpty(configuration) || !_.isEmpty(server_configuration)) {
446
+ cliux.print(
447
+ `\nWARNING!!! The ${name} 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.\n`,
448
+ { color: 'yellow' },
449
+ );
495
450
 
496
- if (!_.isEmpty(config.server_configuration) && !_.isEmpty(serverConfigKeyMapper)) {
497
- _.forEach(_.keys(serverConfigKeyMapper), (key) => {
498
- config.server_configuration[key] = serverConfigKeyMapper[key];
499
- });
451
+ const configOption = this.config.forceStopMarketplaceAppsPrompt
452
+ ? 'Update it with the new configuration.'
453
+ : await cliux.inquire({
454
+ choices: [
455
+ 'Update it with the new configuration.',
456
+ 'Do not update the configuration (WARNING!!! If you do not update the configuration, there may be some issues with the content which you import).',
457
+ 'Exit',
458
+ ],
459
+ type: 'list',
460
+ name: 'value',
461
+ message: 'Choose the option to proceed',
462
+ });
463
+
464
+ if (configOption === 'Exit') {
465
+ process.exit();
466
+ } else if (configOption === 'Update it with the new configuration.') {
467
+ updateParam = { manifest: app.manifest, ...currentStackApp, configuration, server_configuration };
468
+ }
500
469
  }
501
470
 
502
- return config;
471
+ return updateParam;
472
+ }
473
+
474
+ async confirmToCloseProcess(installation) {
475
+ cliux.print(`\nWARNING!!! ${formatError(installation.message)}\n`, { color: 'yellow' });
476
+
477
+ if (!this.config.forceStopMarketplaceAppsPrompt) {
478
+ if (
479
+ !(await cliux.confirm(
480
+ chalk.yellow(
481
+ '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)',
482
+ ),
483
+ ))
484
+ ) {
485
+ process.exit();
486
+ }
487
+ }
503
488
  }
504
489
 
505
490
  /**
506
491
  * @method updateAppsConfig
507
- * @param {Object<{ data, app, httpClient, nodeCrypto }>} param
492
+ * @param {Object<{ data, app }>} param
508
493
  * @returns {Promise<void>}
509
494
  */
510
- updateAppsConfig = ({ data, app, httpClient, nodeCrypto }) => {
511
- const self = this;
512
- return new Promise((resolve, reject) => {
513
- let payload = {};
514
- const { title, configuration, server_configuration } = app;
515
-
516
- if (!_.isEmpty(configuration)) {
517
- payload['configuration'] = nodeCrypto.decrypt(configuration);
518
- }
519
- if (!_.isEmpty(server_configuration)) {
520
- payload['server_configuration'] = nodeCrypto.decrypt(server_configuration);
521
- }
495
+ updateAppsConfig(app) {
496
+ const payload = {};
497
+ const { uid, configuration, server_configuration } = app;
522
498
 
523
- payload = self.updateConfigData(payload);
499
+ if (!_.isEmpty(configuration)) {
500
+ payload['configuration'] = this.nodeCrypto.decrypt(configuration);
501
+ }
502
+ if (!_.isEmpty(server_configuration)) {
503
+ payload['server_configuration'] = this.nodeCrypto.decrypt(server_configuration);
504
+ }
524
505
 
525
- if (_.isEmpty(data) || _.isEmpty(payload) || !data.installation_uid) {
526
- resolve();
527
- } else {
528
- httpClient
529
- .put(`${this.developerHubBaseUrl}/installations/${data.installation_uid}`, payload)
530
- .then(() => {
531
- log(self.config, `${title} app config updated successfully.!`, 'success');
532
- })
533
- .then(resolve)
534
- .catch((error) => {
535
- if (error && (error.message || error.error_message)) {
536
- log(self.config, error.message || error.error_message, 'error');
537
- } else {
538
- log(self.config, 'Something went wrong.!', 'error');
539
- }
540
-
541
- reject();
542
- });
543
- }
544
- });
545
- };
506
+ if (_.isEmpty(app) || _.isEmpty(payload) || !uid) {
507
+ return Promise.resolve();
508
+ }
546
509
 
547
- validateAppName = (name) => {
510
+ return this.httpClient
511
+ .put(`${this.developerHubBaseUrl}/installations/${uid}`, payload)
512
+ .then(({ data }) => {
513
+ if (data.message) {
514
+ log(this.config, formatError(data.message), 'success');
515
+ } else {
516
+ log(this.config, `${app.manifest.name} app config updated successfully.!`, 'success');
517
+ }
518
+ })
519
+ .catch((error) => log(this.config, formatError(error), 'error'));
520
+ }
521
+
522
+ validateAppName(name) {
548
523
  if (name.length < 3 || name.length > 20) {
549
524
  return 'The app name should be within 3-20 characters long.';
550
525
  }
551
526
 
552
527
  return true;
553
- };
528
+ }
554
529
  };