-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmarketplace-apps.ts
More file actions
408 lines (360 loc) · 16.5 KB
/
marketplace-apps.ts
File metadata and controls
408 lines (360 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
import map from 'lodash/map';
import has from 'lodash/has';
import find from 'lodash/find';
import omitBy from 'lodash/omitBy';
import entries from 'lodash/entries';
import isEmpty from 'lodash/isEmpty';
import { resolve as pResolve } from 'node:path';
import { Command } from '@contentstack/cli-command';
import {
cliux,
NodeCrypto,
isAuthenticated,
marketplaceSDKClient,
ContentstackMarketplaceClient,
log,
messageHandler,
handleAndLogError,
} from '@contentstack/cli-utilities';
import BaseClass from './base-class';
import {
fsUtil,
getOrgUid,
createNodeCryptoInstance,
getDeveloperHubUrl,
MODULE_CONTEXTS,
MODULE_NAMES,
PROCESS_NAMES,
PROCESS_STATUS,
} from '../../utils';
import { ModuleClassParams, MarketplaceAppsConfig, ExportConfig, Installation, Manifest } from '../../types';
export default class ExportMarketplaceApps extends BaseClass {
protected marketplaceAppConfig: MarketplaceAppsConfig;
protected installedApps: Installation[] = [];
public developerHubBaseUrl: string;
public marketplaceAppPath: string;
public nodeCrypto: NodeCrypto;
public appSdk: ContentstackMarketplaceClient;
public exportConfig: ExportConfig;
public command: Command;
public query: Record<string, any>;
constructor({ exportConfig, stackAPIClient }: ModuleClassParams) {
super({ exportConfig, stackAPIClient });
this.exportConfig = exportConfig;
this.marketplaceAppConfig = exportConfig.modules.marketplace_apps;
this.exportConfig.context.module = MODULE_CONTEXTS.MARKETPLACE_APPS;
this.currentModuleName = MODULE_NAMES[MODULE_CONTEXTS.MARKETPLACE_APPS];
}
async start(): Promise<void> {
try {
log.debug('Starting marketplace apps export process...', this.exportConfig.context);
if (!isAuthenticated()) {
cliux.print(
'WARNING!!! To export Marketplace apps, you must be logged in. Please check csdx auth:login --help to log in',
{ color: 'yellow' },
);
return Promise.resolve();
}
// Initial setup and analysis with loading spinner
const [appsCount] = await this.withLoadingSpinner('MARKETPLACE-APPS: Analyzing marketplace apps...', async () => {
await this.setupPaths();
const appsCount = await this.getAppsCount();
return [appsCount];
});
if (appsCount === 0) {
log.info(messageHandler.parse('MARKETPLACE_APPS_NOT_FOUND'), this.exportConfig.context);
return;
}
// Handle encryption key prompt BEFORE starting progress
if (!this.exportConfig.forceStopMarketplaceAppsPrompt) {
log.debug('Validating security configuration before progress start', this.exportConfig.context);
cliux.print('\n');
this.nodeCrypto = await createNodeCryptoInstance(this.exportConfig);
cliux.print('\n');
}
// Create nested progress manager
const progress = this.createNestedProgress(this.currentModuleName);
// Add processes based on what we found
progress.addProcess(PROCESS_NAMES.FETCH_APPS, appsCount);
progress.addProcess(PROCESS_NAMES.FETCH_CONFIG_MANIFEST, appsCount); // Manifests and configurations
// Fetch stack specific apps
progress
.startProcess(PROCESS_NAMES.FETCH_APPS)
.updateStatus(PROCESS_STATUS[PROCESS_NAMES.FETCH_APPS].FETCHING, PROCESS_NAMES.FETCH_APPS);
await this.exportApps();
progress.completeProcess(PROCESS_NAMES.FETCH_APPS, true);
// Process apps (manifests and configurations)
if (this.installedApps.length > 0) {
progress
.startProcess(PROCESS_NAMES.FETCH_CONFIG_MANIFEST)
.updateStatus(
PROCESS_STATUS[PROCESS_NAMES.FETCH_CONFIG_MANIFEST].PROCESSING,
PROCESS_NAMES.FETCH_CONFIG_MANIFEST,
);
await this.getAppManifestAndAppConfig();
progress.completeProcess(PROCESS_NAMES.FETCH_CONFIG_MANIFEST, true);
}
this.completeProgressWithMessage();
} catch (error) {
log.debug('Error occurred during marketplace apps export', this.exportConfig.context);
handleAndLogError(error, { ...this.exportConfig.context });
this.completeProgress(false, error?.message || 'Marketplace apps export failed');
}
}
async setupPaths(): Promise<void> {
this.marketplaceAppPath = pResolve(
this.exportConfig.exportDir,
this.exportConfig.branchName || '',
this.marketplaceAppConfig.dirName,
);
log.debug(`Marketplace apps folder path: '${this.marketplaceAppPath}'`, this.exportConfig.context);
await fsUtil.makeDirectory(this.marketplaceAppPath);
log.debug('Created marketplace apps directory', this.exportConfig.context);
this.developerHubBaseUrl = this.exportConfig.developerHubBaseUrl || (await getDeveloperHubUrl(this.exportConfig));
log.debug(`Developer hub base URL: '${this.developerHubBaseUrl}'`, this.exportConfig.context);
this.exportConfig.org_uid = await getOrgUid(this.exportConfig);
this.query = { target_uids: this.exportConfig.apiKey };
log.debug(`Organization UID: '${this.exportConfig.org_uid}'.`, this.exportConfig.context);
// NOTE init marketplace app sdk
const host = this.developerHubBaseUrl.split('://').pop();
log.debug(`Initializing Marketplace SDK with host: '${host}'...`, this.exportConfig.context);
this.appSdk = await marketplaceSDKClient({ host });
}
async getAppsCount(): Promise<number> {
log.debug('Fetching marketplace apps count...', this.exportConfig.context);
try {
const externalQuery = this.exportConfig.query?.modules['marketplace-apps'];
if (externalQuery) {
if (externalQuery.app_uid?.$in?.length > 0) {
this.query.app_uids = externalQuery.app_uid.$in.join(',');
}
if (externalQuery.installation_uid?.$in?.length > 0) {
this.query.installation_uids = externalQuery.installation_uid?.$in?.join(',');
}
}
const collection = await this.appSdk
.marketplace(this.exportConfig.org_uid)
.installation()
.fetchAll({ ...this.query, limit: 1, skip: 0 });
const count = collection?.count || 0;
log.debug(`Total marketplace apps count: ${count}`, this.exportConfig.context);
return count;
} catch (error) {
log.debug('Failed to fetch marketplace apps count', this.exportConfig.context);
return 0;
}
}
/**
* The function `exportApps` encrypts the configuration of installed apps using a Node.js crypto
* library if it is available.
*/
async exportApps(): Promise<any> {
log.debug('Starting apps export process...', this.exportConfig.context);
// Process external query if provided
const externalQuery = this.exportConfig.query?.modules['marketplace-apps'];
if (externalQuery) {
if (externalQuery.app_uid?.$in?.length > 0) {
this.query.app_uids = externalQuery.app_uid.$in.join(',');
}
if (externalQuery.installation_uid?.$in?.length > 0) {
this.query.installation_uids = externalQuery.installation_uid.$in.join(',');
}
}
await this.getStackSpecificApps();
log.debug(`Retrieved ${this.installedApps.length} stack-specific apps`, this.exportConfig.context);
if (!this.nodeCrypto && find(this.installedApps, (app) => !isEmpty(app.configuration))) {
log.debug('Initializing NodeCrypto for app configuration encryption...', this.exportConfig.context);
this.nodeCrypto = await createNodeCryptoInstance(this.exportConfig);
}
this.installedApps = map(this.installedApps, (app) => {
if (has(app, 'configuration')) {
log.debug(`Encrypting configuration for app: '${app.manifest?.name || app.uid}'...`, this.exportConfig.context);
app['configuration'] = this.nodeCrypto.encrypt(app.configuration);
}
return app;
});
log.debug(`Processed ${this.installedApps.length} total marketplace apps`, this.exportConfig.context);
}
/**
* The function `getAppManifestAndAppConfig` exports the manifest and configurations of installed
* marketplace apps.
*/
async getAppManifestAndAppConfig(): Promise<void> {
if (isEmpty(this.installedApps)) {
log.info(messageHandler.parse('MARKETPLACE_APPS_NOT_FOUND'), this.exportConfig.context);
} else {
log.debug(`Processing ${this.installedApps.length} installed apps`, this.exportConfig.context);
for (const [index, app] of entries(this.installedApps)) {
if (app.manifest.visibility === 'private') {
log.debug(`Processing private app manifest: '${app.manifest.name}'...`, this.exportConfig.context);
await this.getPrivateAppsManifest(+index, app);
}
}
for (const [index, app] of entries(this.installedApps)) {
log.debug(`Processing app configurations for: '${app.manifest?.name || app.uid}'...`, this.exportConfig.context);
await this.getAppConfigurations(+index, app);
// Track progress for each app processed
this.progressManager?.tick(
true,
`app: ${app.manifest?.name || app.uid}`,
null,
PROCESS_NAMES.FETCH_CONFIG_MANIFEST,
);
}
const marketplaceAppsFilePath = pResolve(this.marketplaceAppPath, this.marketplaceAppConfig.fileName);
log.debug(`Writing Marketplace Apps to: '${marketplaceAppsFilePath}'`, this.exportConfig.context);
fsUtil.writeFile(marketplaceAppsFilePath, this.installedApps);
log.success(
messageHandler.parse('MARKETPLACE_APPS_EXPORT_COMPLETE', Object.keys(this.installedApps || {}).length),
this.exportConfig.context,
);
}
}
/**
* The function `getPrivateAppsManifest` fetches the manifest of a private app and assigns it to the
* `manifest` property of the corresponding installed app.
* @param {number} index - The `index` parameter is a number that represents the position of the app
* in an array or list. It is used to identify the specific app in the `installedApps` array.
* @param {App} appInstallation - The `appInstallation` parameter is an object that represents the
* installation details of an app. It contains information such as the UID (unique identifier) of the
* app's manifest.
*/
async getPrivateAppsManifest(index: number, appInstallation: Installation) {
log.debug(
`Fetching private app manifest for: '${appInstallation.manifest.name}' (${appInstallation.manifest.uid})`,
this.exportConfig.context,
);
const manifest = await this.appSdk
.marketplace(this.exportConfig.org_uid)
.app(appInstallation.manifest.uid)
.fetch({ include_oauth: true })
.catch((error) => {
log.debug(
`Failed to fetch private app manifest for: '${appInstallation.manifest.name}'`,
this.exportConfig.context,
);
handleAndLogError(
error,
{
...this.exportConfig.context,
},
messageHandler.parse('MARKETPLACE_APP_MANIFEST_EXPORT_FAILED', appInstallation.manifest.name),
);
});
if (manifest) {
log.debug(
`Successfully fetched private app manifest for: '${appInstallation.manifest.name}'`,
this.exportConfig.context,
);
this.installedApps[index].manifest = manifest as unknown as Manifest;
}
}
/**
* The function `getAppConfigurations` exports the configuration of an app installation and encrypts
* the server configuration if it exists.
* @param {number} index - The `index` parameter is a number that represents the index of the app
* installation in an array or list. It is used to identify the specific app installation that needs
* to be processed or accessed.
* @param {any} appInstallation - The `appInstallation` parameter is an object that represents the
* installation details of an app. It contains information such as the app's manifest, unique
* identifier (uid), and other installation data.
*/
async getAppConfigurations(index: number, appInstallation: any) {
const appName = appInstallation?.manifest?.name;
const appUid = appInstallation?.manifest?.uid;
const app = appName || appUid;
log.debug(`Fetching app configuration for: '${app}'...`, this.exportConfig.context);
log.info(messageHandler.parse('MARKETPLACE_APP_CONFIG_EXPORT', app), this.exportConfig.context);
await this.appSdk
.marketplace(this.exportConfig.org_uid)
.installation(appInstallation.uid)
.installationData()
.then(async (result: any) => {
const { data, error } = result;
if (has(data, 'server_configuration') || has(data, 'configuration')) {
log.debug(`Found configuration data for app: '${app}'`, this.exportConfig.context);
if (!this.nodeCrypto && (has(data, 'server_configuration') || has(data, 'configuration'))) {
log.debug(`Initializing NodeCrypto for app: '${app}'...`, this.exportConfig.context);
this.nodeCrypto = await createNodeCryptoInstance(this.exportConfig);
this.progressManager?.updateStatus(
PROCESS_STATUS[PROCESS_NAMES.FETCH_CONFIG_MANIFEST].PROCESSING,
PROCESS_NAMES.FETCH_CONFIG_MANIFEST,
);
}
if (!isEmpty(data?.configuration)) {
log.debug(`Encrypting configuration for app: '${app}'...`, this.exportConfig.context);
this.installedApps[index]['configuration'] = this.nodeCrypto.encrypt(data.configuration);
}
if (!isEmpty(data?.server_configuration)) {
log.debug(`Encrypting server configuration for app: '${app}'...`, this.exportConfig.context);
this.installedApps[index]['server_configuration'] = this.nodeCrypto.encrypt(data.server_configuration);
log.success(messageHandler.parse('MARKETPLACE_APP_CONFIG_SUCCESS', app), this.exportConfig.context);
} else {
log.success(messageHandler.parse('MARKETPLACE_APP_EXPORT_SUCCESS', app), this.exportConfig.context);
}
} else if (error) {
log.debug(`Error in app configuration data for: '${app}'.`, this.exportConfig.context);
handleAndLogError(
error,
{
...this.exportConfig.context,
},
messageHandler.parse('MARKETPLACE_APP_CONFIG_EXPORT_FAILED', app),
);
}
})
.catch((error: any) => {
log.debug(`Failed to fetch app configuration for: '${app}'.`, this.exportConfig.context);
handleAndLogError(
error,
{
...this.exportConfig.context,
},
messageHandler.parse('MARKETPLACE_APP_CONFIG_EXPORT_FAILED', app),
);
});
}
/**
* The function `getStackSpecificApps` retrieves a collection of marketplace apps specific to a stack
* and stores them in the `installedApps` array.
* @param [skip=0] - The `skip` parameter is used to determine the number of items to skip in the API
* response. It is used for pagination purposes, allowing you to fetch a specific range of items from
* the API. In this code, it is initially set to 0, indicating that no items should be skipped in
*/
async getStackSpecificApps(skip = 0) {
log.debug(`Fetching stack-specific apps with skip: ${skip}`, this.exportConfig.context);
const collection = await this.appSdk
.marketplace(this.exportConfig.org_uid)
.installation()
.fetchAll({ ...this.query, skip })
.catch((error) => {
log.debug('An error occurred while fetching stack-specific apps.', this.exportConfig.context);
handleAndLogError(error, {
...this.exportConfig.context,
});
});
if (collection) {
const { items: apps, count } = collection;
log.debug(`Fetched ${apps?.length || 0} apps out of total ${count}`, this.exportConfig.context);
// NOTE Remove all the chain functions
const installation = map(apps, (app) =>
omitBy(app, (val, _key) => {
if (val instanceof Function) return true;
return false;
}),
) as unknown as Installation[];
log.debug(`Processed ${installation.length} app installations`, this.exportConfig.context);
// Track progress for each app fetched
installation.forEach((app) => {
this.progressManager?.tick(true, `app: ${app.manifest?.name || app.uid}`, null, PROCESS_NAMES.FETCH_APPS);
});
this.installedApps = this.installedApps.concat(installation);
if (count - (skip + 50) > 0) {
log.debug(`Continuing to fetch apps with skip: ${skip + 50}.`, this.exportConfig.context);
await this.getStackSpecificApps(skip + 50);
} else {
log.debug('Completed fetching all stack-specific apps.', this.exportConfig.context);
}
}
}
}