From 2bc2d3edec56511687b8e1b7df3740798432f270 Mon Sep 17 00:00:00 2001 From: Antony Date: Wed, 24 Jan 2024 14:59:31 +0530 Subject: [PATCH 1/2] Refactor: Manifest related changes --- src/commands/app/create.ts | 64 ++++++++++++++++++---- src/commands/app/update.ts | 12 +++-- src/config/manifest.json | 76 ++++++++++++++++++++------- test/unit/commands/app/create.test.ts | 26 +++------ 4 files changed, 125 insertions(+), 53 deletions(-) diff --git a/src/commands/app/create.ts b/src/commands/app/create.ts index f824efd..974b94d 100644 --- a/src/commands/app/create.ts +++ b/src/commands/app/create.ts @@ -1,5 +1,6 @@ import * as tmp from "tmp"; import AdmZip from "adm-zip"; +import omit from "lodash/omit"; import pick from "lodash/pick"; import * as shell from "shelljs"; import merge from "lodash/merge"; @@ -14,7 +15,13 @@ import { writeFileSync, createWriteStream, } from "fs"; -import { ux, cliux, flags, HttpClient, configHandler } from "@contentstack/cli-utilities"; +import { + ux, + cliux, + flags, + HttpClient, + configHandler, +} from "@contentstack/cli-utilities"; import { BaseCommand } from "../../base-command"; import { AppManifest, AppType } from "../../types"; @@ -28,6 +35,11 @@ import { export default class Create extends BaseCommand { private appData!: AppManifest; + private tempAppData = { + name: "", + target_type: "", + ui_location: { locations: undefined }, + } as any; static description = "Create a new app in Developer Hub and optionally clone a boilerplate locally."; @@ -62,15 +74,14 @@ export default class Create extends BaseCommand { async run(): Promise { this.sharedConfig.org = this.flags.org; this.sharedConfig.appName = this.flags.name; - this.appData = require(this.sharedConfig.manifestPath); await this.flagsPromptQueue(); - this.appData.name = this.sharedConfig.appName; - this.appData.target_type = this.flags["app-type"] as AppType; + this.tempAppData.name = this.sharedConfig.appName; + this.tempAppData.target_type = this.flags["app-type"] as AppType; if (this.flags["app-type"] === AppType.ORGANIZATION) { - this.appData.ui_location.locations = getOrgAppUiLocation(); + this.tempAppData.ui_location.locations = getOrgAppUiLocation(); } try { @@ -84,6 +95,7 @@ export default class Create extends BaseCommand { ) { await this.boilerplateFlow(); } else { + this.manageManifestToggeling(); await this.registerTheAppOnDeveloperHub(false); } } catch (error: Error | any) { @@ -105,6 +117,8 @@ export default class Create extends BaseCommand { await this.unZipBoilerplate(await this.cloneBoilerplate()); tmp.setGracefulCleanup(); // NOTE If graceful cleanup is set, tmp will remove all controlled temporary objects on process exit + this.manageManifestToggeling(); + // NOTE Step 2: Registering the app await this.registerTheAppOnDeveloperHub(); @@ -114,7 +128,7 @@ export default class Create extends BaseCommand { ux.action.stop(); this.log( this.$t(this.messages.START_APP_COMMAND, { - command: `cd ${this.sharedConfig.folderPath} && npm run start`, + command: `cd "${this.sharedConfig.folderPath}" && npm run start`, }), "info" ); @@ -133,10 +147,12 @@ export default class Create extends BaseCommand { } //Auto select org in case of oauth - this.sharedConfig.org = configHandler.get('oauthOrgUid') ?? (await getOrg(this.flags, { - log: this.log, - managementSdk: this.managementSdk, - })); + this.sharedConfig.org = + configHandler.get("oauthOrgUid") ?? + (await getOrg(this.flags, { + log: this.log, + managementSdk: this.managementSdk, + })); } /** @@ -208,6 +224,34 @@ export default class Create extends BaseCommand { }); } + /** + * @method manageManifestToggeling + * + * The function manages toggling of the manifest file based on the app type, removing the + * "ui_location" property if the app type is an organization. + */ + manageManifestToggeling() { + // NOTE Use boilerplate manifest if exist + const manifestPath = resolve( + this.sharedConfig.folderPath || "", + "manifest.json" + ); + + if (existsSync(manifestPath)) { + this.sharedConfig.manifestPath = manifestPath; + } + + let manifest = require(this.sharedConfig.manifestPath); + + if (this.flags["app-type"] === AppType.ORGANIZATION) { + manifest = omit(manifest, ["ui_location"]); + } else { + this.tempAppData = omit(this.tempAppData, ["ui_location"]); + } + + this.appData = merge(manifest, this.tempAppData); + } + /** * @method registerTheAppOnDeveloperHub * diff --git a/src/commands/app/update.ts b/src/commands/app/update.ts index 05d3a25..c4a8ef2 100644 --- a/src/commands/app/update.ts +++ b/src/commands/app/update.ts @@ -7,7 +7,7 @@ import { existsSync, readFileSync, writeFileSync } from "fs"; import { $t, appUpdate } from "../../messages"; import { fetchApp, getApp, getOrg } from "../../util"; -import {AppCLIBaseCommand} from "../../app-cli-base-coomand"; +import { AppCLIBaseCommand } from "../../app-cli-base-coomand"; export default class Update extends AppCLIBaseCommand { private orgUid!: string; @@ -28,12 +28,14 @@ export default class Update extends AppCLIBaseCommand { async run(): Promise { try { - //if working directory isn't app directory - if(!this.manifestData){ + // if working directory isn't app directory + if (!this.manifestData) { await this.validateManifest(); } - this.flags["app-manifest"] = this.manifestPath ?? this.flags["app-manifest"]; - this.orgUid = this.flags.org ?? this.manifestData?.organization_uid; + + this.flags["app-manifest"] = + this.manifestPath ?? this.flags["app-manifest"]; + this.orgUid = this.flags.org ?? this.manifestData?.organization_uid; this.sharedConfig.org = await getOrg( { org: this.orgUid as any }, { diff --git a/src/config/manifest.json b/src/config/manifest.json index 2b501d9..30f03ec 100644 --- a/src/config/manifest.json +++ b/src/config/manifest.json @@ -1,10 +1,11 @@ { - "icon": "", "description": "", - "target_type": "stack", + "icon": "", "name": "", - "visibility": "private", "organization_uid": "", + "target_type": "stack", + "visibility": "private", + "uid": "", "ui_location": { "signed": false, "base_url": "http://localhost:3000", @@ -13,20 +14,22 @@ "type": "cs.cm.stack.custom_field", "meta": [ { - "path": "/custom-field", - "signed": true, + "multiple": false, + "path": "/#/custom-field", + "signed": false, "enabled": true, - "data_type": "number" + "data_type": "json" } ] }, { - "type": "cs.cm.stack.config", + "type": "cs.cm.stack.dashboard", "meta": [ { - "path": "/app-configuration", - "signed": true, - "enabled": true + "path": "/#/stack-dashboard", + "signed": false, + "enabled": true, + "default_width": "half" } ] }, @@ -35,35 +38,68 @@ "meta": [ { "blur": false, - "path": "/asset-sidebar", - "signed": true, + "path": "/#/asset-sidebar", + "signed": false, "enabled": true, "width": 500 } ] }, { - "type": "cs.cm.stack.dashboard", + "type": "cs.cm.stack.sidebar", "meta": [ { - "path": "/stack-dashboard", - "signed": true, + "path": "/#/entry-sidebar", + "signed": false, + "enabled": true + } + ] + }, + { + "type": "cs.cm.stack.full_page", + "meta": [ + { + "path": "/#/full-page", + "signed": false, + "enabled": true + } + ] + }, + { + "type": "cs.cm.stack.field_modifier", + "meta": [ + { + "path": "/#/field-modifier", + "signed": false, "enabled": true, - "default_width": "half" + "allowed_types": ["$all"] } ] }, { - "type": "cs.cm.stack.sidebar", + "type": "cs.cm.stack.config", + "meta": [ + { + "path": "/#/app-configuration", + "signed": false, + "enabled": true + } + ] + }, + { + "type": "cs.cm.stack.rte", "meta": [ { - "path": "/entry-sidebar", - "signed": true, + "path": "/json-rte.js", + "signed": false, "enabled": true } ] } ] }, - "uid": "" + "hosting": { + "provider": "external", + "deployment_url": "http://localhost:3000" + } } diff --git a/test/unit/commands/app/create.test.ts b/test/unit/commands/app/create.test.ts index a6d0ff3..587a6f5 100644 --- a/test/unit/commands/app/create.test.ts +++ b/test/unit/commands/app/create.test.ts @@ -13,7 +13,7 @@ import messages from "../../../../src/messages"; import * as mock from "../../mock/common.mock.json"; import manifestData from "../../../../src/config/manifest.json"; -const gitHubHost = "https://github.com"; +const { origin, pathname } = new URL(config.appBoilerplateGithubUrl); const zipPath = join(process.cwd(), "test", "unit", "mock", "boilerplate.zip"); const region: { cma: string; name: string; cda: string } = configHandler.get("region"); @@ -50,10 +50,8 @@ describe("app:create", () => { return (cases as Record)[prompt.name]; }) - .nock(gitHubHost, (api) => - api - .get(config.appBoilerplateGithubUrl.replace(gitHubHost, "")) - .reply(200, { data: "test-data" }) + .nock(origin, (api) => + api.get(pathname).reply(200, { data: "test-data" }) ) .nock(region.cma, (api) => api @@ -98,10 +96,8 @@ describe("app:create", () => { return (cases as Record)[prompt.name]; }) - .nock(gitHubHost, (api) => - api - .get(config.appBoilerplateGithubUrl.replace(gitHubHost, "")) - .reply(200, { data: "test-data" }) + .nock(origin, (api) => + api.get(pathname).reply(200, { data: "test-data" }) ) .nock(region.cma, (api) => api @@ -195,11 +191,7 @@ describe("app:create", () => { return (cases as Record)[prompt.name]; }) - .nock(gitHubHost, (api) => - api - .get(config.appBoilerplateGithubUrl.replace(gitHubHost, "")) - .reply(200) - ) + .nock(origin, (api) => api.get(pathname).reply(200)) .nock(region.cma, (api) => api .get("/v3/organizations?limit=100&asc=name&include_count=true&skip=0") @@ -305,10 +297,8 @@ describe("app:create", () => { return (cases as Record)[prompt.name]; }) - .nock(gitHubHost, (api) => - api - .get(config.appBoilerplateGithubUrl.replace(gitHubHost, "")) - .reply(200, { data: "test-data" }) + .nock(origin, (api) => + api.get(pathname).reply(200, { data: "test-data" }) ) .nock(region.cma, (api) => api From 2c7c8191a5504489cc86e11f6e1e1857e19d4237 Mon Sep 17 00:00:00 2001 From: Antony Date: Wed, 24 Jan 2024 19:22:39 +0530 Subject: [PATCH 2/2] Fix: Code lint fix, formate applied, unit test fixes --- src/util/common-utils.ts | 62 ++++++++------- src/util/inquirer.ts | 166 ++++++++++++++++++++++----------------- 2 files changed, 131 insertions(+), 97 deletions(-) diff --git a/src/util/common-utils.ts b/src/util/common-utils.ts index c4a661f..8c8c708 100644 --- a/src/util/common-utils.ts +++ b/src/util/common-utils.ts @@ -97,16 +97,16 @@ function fetchAppInstallations( const { managementSdk } = options; const app: any = flags["app-uid"]; return managementSdk - .organization(orgUid) - .app(app as string) - .installation() - .findAll() - .catch(error => { - const {log} = options; - cliux.loader("failed"); - log("Some error occurred while fetching app installations.", "warn"); - throw error // throwing error here instead of removing the catch block, as the loader needs to stopped in case there is an error. - }) + .organization(orgUid) + .app(app as string) + .installation() + .findAll() + .catch((error) => { + const { log } = options; + cliux.loader("failed"); + log("Some error occurred while fetching app installations.", "warn"); + throw error; // throwing error here instead of removing the catch block, as the loader needs to stopped in case there is an error. + }); } function deleteApp(flags: FlagInput, orgUid: string, options: CommonOptions) { @@ -144,10 +144,10 @@ function fetchStack(flags: FlagInput, options: CommonOptions) { async function getStacks( options: CommonOptions, orgUid: string, - skip: number= 0, - stacks: Stack[] = [], + skip: number = 0, + stacks: Stack[] = [] ): Promise { - const {log, managementSdk} = options; + const { log, managementSdk } = options; const response = await managementSdk .organization(orgUid) .stacks({ include_count: true, limit: 100, asc: "name", skip: skip }) @@ -168,17 +168,26 @@ async function getStacks( return stacks; } -function uninstallApp(flags: FlagInput, orgUid: string, options: CommonOptions, installationUid: string) { - const {managementSdk} = options; - const app: unknown = flags['app-uid']; +function uninstallApp( + flags: FlagInput, + orgUid: string, + options: CommonOptions, + installationUid: string +) { + const { managementSdk } = options; + const app: unknown = flags["app-uid"]; return managementSdk - .organization(orgUid) - .app(app as string) - .installation(installationUid as string) - .uninstall() + .organization(orgUid) + .app(app as string) + .installation(installationUid as string) + .uninstall(); } -async function fetchInstalledApps(flags: FlagInput, orgUid: string, options: CommonOptions) { +async function fetchInstalledApps( + flags: FlagInput, + orgUid: string, + options: CommonOptions +) { const { managementSdk, log } = options; const apps = (await fetchApps(flags, orgUid, options)) || []; let batchRequests = []; @@ -196,8 +205,7 @@ async function fetchInstalledApps(flags: FlagInput, orgUid: string, options: Com .installation() .findAll(); return installations.items.length ? installations.items : null; - } - catch (error) { + } catch (error) { log("Unable to fetch installations.", "warn"); log(error, "error"); throw error; @@ -217,10 +225,10 @@ async function fetchInstalledApps(flags: FlagInput, orgUid: string, options: Com return batchRequests.flat(); } -export { - getOrganizations, - getOrgAppUiLocation, - fetchApps, +export { + getOrganizations, + getOrgAppUiLocation, + fetchApps, fetchApp, fetchAppInstallations, deleteApp, diff --git a/src/util/inquirer.ts b/src/util/inquirer.ts index 14667e7..a00d1df 100644 --- a/src/util/inquirer.ts +++ b/src/util/inquirer.ts @@ -7,20 +7,20 @@ import { FlagInput, configHandler, Stack, - ContentstackClient + ContentstackClient, } from "@contentstack/cli-utilities"; -import {Installation} from '@contentstack/management/types/app/installation' -import {AppTarget} from '@contentstack/management/types/app/index' +import { Installation } from "@contentstack/management/types/app/installation"; +import { AppTarget } from "@contentstack/management/types/app/index"; import config from "../config"; import messages, { $t, commonMsg, errors, uninstallAppMsg } from "../messages"; -import { - CommonOptions, - getOrganizations, +import { + CommonOptions, + getOrganizations, getStacks, fetchAppInstallations, fetchInstalledApps, - fetchApps + fetchApps, } from "./common-utils"; /** @@ -87,7 +87,7 @@ async function getOrg(flags: FlagInput, options: CommonOptions) { if (!(flags.org && find(organizations, { uid: flags.org }))) { if (flags.org) { - throw new Error(messages.ORG_UID_NOT_FOUND) + throw new Error(messages.ORG_UID_NOT_FOUND); } flags.org = await cliux @@ -103,46 +103,54 @@ async function getOrg(flags: FlagInput, options: CommonOptions) { return flags.org; } -async function getApp(flags: FlagInput, orgUid: string, options: CommonOptions) : Promise | undefined> { +async function getApp( + flags: FlagInput, + orgUid: string, + options: CommonOptions +): Promise | undefined> { cliux.loader("Loading Apps"); - const apps = (await fetchApps(flags, orgUid, options)); + const apps = await fetchApps(flags, orgUid, options); cliux.loader("done"); - + if (apps.length === 0) { - throw new Error(messages.APPS_NOT_FOUND) + throw new Error(messages.APPS_NOT_FOUND); } - + flags.app = await cliux .inquire({ type: "search-list", name: "App", choices: apps, - message: messages.CHOOSE_APP + message: messages.CHOOSE_APP, }) - .then((name) => apps.find(app => app.name === name)?.uid) + .then((name) => apps.find((app) => app.name === name)?.uid); - return apps.find(app => app.uid === flags.app); + return apps.find((app) => app.uid === flags.app); } -async function getInstalledApps(flags: FlagInput, orgUid: string, options: CommonOptions) : Promise | undefined> { +async function getInstalledApps( + flags: FlagInput, + orgUid: string, + options: CommonOptions +): Promise | undefined> { cliux.loader("Loading Apps"); - const apps = (await fetchInstalledApps(flags, orgUid, options)); + const apps = await fetchInstalledApps(flags, orgUid, options); cliux.loader("done"); - + if (apps.length === 0) { - throw new Error(messages.APPS_NOT_FOUND) + throw new Error(messages.APPS_NOT_FOUND); } - + flags.app = await cliux .inquire({ type: "search-list", name: "App", choices: apps, - message: messages.CHOOSE_APP + message: messages.CHOOSE_APP, }) - .then((name) => apps.find(app => app.name === name)?.uid) + .then((name) => apps.find((app) => app.name === name)?.uid); - return apps.find(app => app.uid === flags.app); + return apps.find((app) => app.uid === flags.app); } /** @@ -176,107 +184,125 @@ async function getDeveloperHubUrl(): Promise { return developerHubBaseUrl; } -async function getStack(orgUid: string, options: CommonOptions): Promise | undefined> { +async function getStack( + orgUid: string, + options: CommonOptions +): Promise | undefined> { cliux.loader("Loading Stacks"); const stacks = (await getStacks(options, orgUid)) || []; cliux.loader("done"); - + if (stacks.length === 0) { // change this to stacks not found - throw new Error(messages.APPS_NOT_FOUND) + throw new Error(messages.APPS_NOT_FOUND); } - + const selectedStack = await cliux .inquire({ type: "search-list", name: "Stack", choices: stacks, - message: messages.CHOOSE_A_STACK + message: messages.CHOOSE_A_STACK, }) - .then((name) => stacks.find(stack => stack.name === name)) + .then((name) => stacks.find((stack) => stack.name === name)); return selectedStack; } async function getInstallation( - flags: FlagInput, - orgUid: string, + flags: FlagInput, + orgUid: string, managementSdkForStacks: ContentstackClient, appType: AppTarget, - options:CommonOptions, - uninstallAll?: boolean, -) : Promise { - const {log} = options; - if (appType === 'stack') { + options: CommonOptions, + uninstallAll?: boolean +): Promise { + const { log } = options; + if (appType === "stack") { cliux.loader("Loading App Installations"); } - let {items: installations} = (await fetchAppInstallations(flags, orgUid, options)) || []; + let { items: installations } = (await fetchAppInstallations( + flags, + orgUid, + options + )) || { items: [] }; - // console.log(installations) if (!installations?.length) { - if (appType === "stack") cliux.loader("done") - throw new Error(messages.NO_INSTALLATIONS_FOUND) + if (appType === "stack") cliux.loader("done"); + throw new Error(messages.NO_INSTALLATIONS_FOUND); } let selectedInstallation: string; - if (appType === 'stack') { + if (appType === "stack") { // fetch stacks from where the app has to be uninstalled cliux.loader("done"); - const stacks: Stack[] = await getStacks({managementSdk: managementSdkForStacks, log: options.log}, orgUid); - installations = populateMissingDataInInstallations(installations as [Installation], stacks) + const stacks: Stack[] = await getStacks( + { managementSdk: managementSdkForStacks, log: options.log }, + orgUid + ); + installations = populateMissingDataInInstallations( + installations as [Installation], + stacks + ); // To support uninstall all flag if (uninstallAll) { - return installations.map(installation => installation.uid).join(',') + return installations.map((installation) => installation.uid).join(","); } - let _selectedInstallation: string[] = await cliux - .inquire({ - type: 'checkbox', - name: 'appInstallation', + let _selectedInstallation: string[] = await cliux.inquire({ + type: "checkbox", + name: "appInstallation", choices: installations, message: messages.CHOOSE_AN_INSTALLATION, validate: (input) => { if (isEmpty(input)) { - return $t(errors.NOT_EMPTY, { value: "stack value"}); + return $t(errors.NOT_EMPTY, { value: "stack value" }); } return true; - } - }) - selectedInstallation = _selectedInstallation.join(',') + }, + }); + selectedInstallation = _selectedInstallation.join(","); } else { // as this is an organization app, and it is supposed to only be installed on the source organization // it will be uninstalled from the selected organization - selectedInstallation = installations.pop()?.uid || "" + selectedInstallation = installations.pop()?.uid || ""; } - log($t(uninstallAppMsg.UNINSTALLING_APP, { - type: appType - }), "info") - - return selectedInstallation + log( + $t(uninstallAppMsg.UNINSTALLING_APP, { + type: appType, + }), + "info" + ); + + return selectedInstallation; } -function populateMissingDataInInstallations(installations: [Installation], stacks: Stack[]): [Installation] { - let result = installations.map(installation => { - let stack = stacks.filter(stack => stack.api_key === installation.target.uid)?.pop() +function populateMissingDataInInstallations( + installations: [Installation], + stacks: Stack[] +): [Installation] { + let result = installations.map((installation) => { + let stack = stacks + .filter((stack) => stack.api_key === installation.target.uid) + ?.pop(); installation.name = stack?.name || installation.target.uid; installation.value = installation.uid; return installation; }) as [Installation]; if (result.length > 0) { - return result + return result; } - return installations - + return installations; } -export { - getOrg, - getAppName, - getDirName, - getDeveloperHubUrl, +export { + getOrg, + getAppName, + getDirName, + getDeveloperHubUrl, getApp, getInstalledApps, getStack,