diff --git a/packages/cli/src/cli/commands/documents.ts b/packages/cli/src/cli/commands/documents.ts new file mode 100644 index 00000000..aabac1b4 --- /dev/null +++ b/packages/cli/src/cli/commands/documents.ts @@ -0,0 +1,116 @@ +import { exit } from "process"; +import chalk from "chalk"; +import AddOnApiHelper from "../../lib/addonApiHelper"; +import config from "../../lib/config"; +import { Logger, SpinnerLogger } from "../../lib/logger"; +import { filterUndefinedProperties, parameterize } from "../../lib/utils"; +import { errorHandler } from "../exceptions"; + +type GeneratePreviewParam = { + documentId: string; + baseUrl: string; +}; +function generateBaseAPIPath(siteData: Site, baseUrl?: string) { + if (!siteData) return "#"; + + const isPlayground = siteData.__isPlayground; + + let _baseUrl: string; + if (baseUrl) _baseUrl = baseUrl; + else if (isPlayground) _baseUrl = config.playgroundUrl; + else _baseUrl = siteData.url; + + return isPlayground + ? `${_baseUrl}/api/${siteData.id}/pantheoncloud` + : `${_baseUrl}/api/pantheoncloud`; +} + +async function generateDocumentPath( + site: Site, + docId: string, + isPreview: boolean, + { + baseUrl, + queryParams, + }: { + baseUrl?: string; + queryParams?: Record; + }, +) { + const augmentedQueryParams = { ...queryParams }; + + if (isPreview) { + augmentedQueryParams.pccGrant = await AddOnApiHelper.getPreviewJwt(site.id); + } + + const params = + augmentedQueryParams == null + ? {} + : filterUndefinedProperties(augmentedQueryParams); + + return `${generateBaseAPIPath(site, baseUrl)}/document/${docId}${ + Object.values(params).length > 0 ? `/?${parameterize(params)}` : "" + }`; +} + +export const generatePreviewLink = errorHandler( + async ({ documentId, baseUrl }: GeneratePreviewParam) => { + let document: Article; + const logger = new Logger(); + + if (baseUrl) { + try { + new URL(baseUrl); + } catch (_err) { + logger.error( + chalk.red( + `ERROR: Value provided for \`baseUrl\` is not a valid URL. `, + ), + ); + exit(1); + } + } + + // Fetching document details + const fetchLogger = new SpinnerLogger("Fetching document details..."); + fetchLogger.start(); + try { + document = await AddOnApiHelper.getDocument(documentId); + } catch (err) { + fetchLogger.stop(); + if ((err as { response: { status: number } }).response.status === 404) { + logger.error( + chalk.red("ERROR: Article not found for given document ID."), + ); + exit(1); + } else throw err; + } + let site: Site; + try { + site = await AddOnApiHelper.getSite(document.siteId); + } catch (err) { + fetchLogger.stop(); + if ((err as { response: { status: number } }).response.status === 404) { + logger.error(chalk.red("ERROR: Site not found for given document.")); + exit(1); + } else throw err; + } + fetchLogger.succeed("Fetched document details!"); + + // Generating link + const generateLinkLogger = new SpinnerLogger("Generating preview link"); + generateLinkLogger.start(); + + const buildLink = `${await generateDocumentPath(site, documentId, true, { + queryParams: { + publishingLevel: "REALTIME", + }, + baseUrl, + })}`; + generateLinkLogger.succeed( + "Successfully generated preview link. Please copy it from below:", + ); + + logger.log(chalk.green(buildLink)); + }, +); diff --git a/packages/cli/src/cli/index.ts b/packages/cli/src/cli/index.ts index 5fafea14..be1dd460 100755 --- a/packages/cli/src/cli/index.ts +++ b/packages/cli/src/cli/index.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import yargs from "yargs"; import { hideBin } from "yargs/helpers"; +import { generatePreviewLink } from "./commands/documents"; import init from "./commands/init"; import login from "./commands/login"; import logout from "./commands/logout"; @@ -141,7 +142,7 @@ yargs(hideBin(process.argv)) "Revokes token for a given id.", (yargs) => { yargs.positional("", { - describe: "ID of the token which you want to revoke", + describe: "ID of the token which you want to revoke.", demandOption: true, type: "string", }); @@ -177,13 +178,13 @@ yargs(hideBin(process.argv)) "Shows component schema of the site.", (yargs) => { yargs.option("url", { - describe: "Site url", + describe: "Site url.", type: "string", demandOption: true, }); yargs.option("apiPath", { - describe: "API path such as /api/pantheoncloud/component_schema", + describe: "API path such as /api/pantheoncloud/component_schema.", type: "string", demandOption: false, }); @@ -204,12 +205,12 @@ yargs(hideBin(process.argv)) ) .command( "configure [options]", - "Configure properties for a given site", + "Configure properties for a given site.", (yargs) => { yargs .strictCommands() .positional("", { - describe: "ID of the site which you want to configure", + describe: "ID of the site which you want to configure.", demandOption: true, type: "string", }) @@ -245,24 +246,25 @@ yargs(hideBin(process.argv)) ) .command( "webhooks [options]", - "Manage webhooks for a given site", + "Manage webhooks for a given site.", (yargs) => { yargs .strictCommands() .demandCommand() .command( "history ", - "View webhook event delivery logs for a given site", + "View webhook event delivery logs for a given site.", (yargs) => { yargs .strictCommands() .positional("", { - describe: "ID of the site for which you want to see logs", + describe: + "ID of the site for which you want to see logs.", demandOption: true, type: "string", }) .option("limit", { - describe: "Number of logs to fetch at a time", + describe: "Number of logs to fetch at a time.", type: "number", default: 100, demandOption: false, @@ -281,6 +283,38 @@ yargs(hideBin(process.argv)) // noop }, ) + .command( + "document [options]", + "Enables you to manage documents for a PCC Project.", + (yargs) => { + yargs + .strictCommands() + .demandCommand() + .command( + "preview ", + "Generates preview link for a given document ID.", + (yargs) => { + yargs + .strictCommands() + .positional("", { + describe: "ID of the document.", + demandOption: true, + type: "string", + }) + .option("baseUrl", { + describe: "Base URL for the generated preview link.", + type: "string", + demandOption: false, + }); + }, + async (args) => + await generatePreviewLink({ + documentId: args.id as string, + baseUrl: args.baseUrl as string, + }), + ); + }, + ) .command( "login", "Logs you in you to PCC client.", diff --git a/packages/cli/src/lib/addonApiHelper.ts b/packages/cli/src/lib/addonApiHelper.ts index ce698fcd..d94374f8 100644 --- a/packages/cli/src/lib/addonApiHelper.ts +++ b/packages/cli/src/lib/addonApiHelper.ts @@ -6,6 +6,7 @@ import { getLocalAuthDetails } from "./localStorage"; const API_KEY_ENDPOINT = `${config.addOnApiEndpoint}/api-key`; const SITE_ENDPOINT = `${config.addOnApiEndpoint}/sites`; +const DOCUMENT_ENDPOINT = `${config.addOnApiEndpoint}/articles`; const OAUTH_ENDPOINT = `${config.addOnApiEndpoint}/oauth`; class AddOnApiHelper { @@ -22,6 +23,32 @@ class AddOnApiHelper { return resp.data as Credentials; } + static async getDocument(documentId: string): Promise
{ + const authDetails = await getLocalAuthDetails(); + if (!authDetails) throw new UserNotLoggedIn(); + + const resp = await axios.get(`${DOCUMENT_ENDPOINT}/${documentId}`, { + headers: { + Authorization: `Bearer ${authDetails.id_token}`, + }, + }); + + return resp.data as Article; + } + + static async getPreviewJwt(siteId: string): Promise { + const authDetails = await getLocalAuthDetails(); + if (!authDetails) throw new UserNotLoggedIn(); + + const resp = await axios.post(`${SITE_ENDPOINT}/${siteId}/preview`, null, { + headers: { + Authorization: `Bearer ${authDetails.id_token}`, + }, + }); + + return resp.data.grantToken as string; + } + static async createApiKey(): Promise { const authDetails = await getLocalAuthDetails(); if (!authDetails) throw new UserNotLoggedIn(); @@ -98,6 +125,18 @@ class AddOnApiHelper { return resp.data as Site[]; } + static async getSite(siteId: string): Promise { + const authDetails = await getLocalAuthDetails(); + if (!authDetails) throw new UserNotLoggedIn(); + + const resp = await axios.get(`${SITE_ENDPOINT}/${siteId}`, { + headers: { + Authorization: `Bearer ${authDetails.id_token}`, + }, + }); + + return resp.data as Site; + } static async updateSite(id: string, url: string): Promise { const authDetails = await getLocalAuthDetails(); diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index a62c2c17..5a1b559f 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -7,6 +7,7 @@ type Config = { addOnApiEndpoint: string; googleClientId: string; googleRedirectUri: string; + playgroundUrl: string; }; const ENV: Env = (process.env.NODE_ENV as Env) || "production"; @@ -17,6 +18,7 @@ const config: { [key in Env]: Config } = { googleClientId: "432998952749-6eurouamlt7mvacb6u4e913m3kg4774c.apps.googleusercontent.com", googleRedirectUri: "http://localhost:3030/oauth-redirect", + playgroundUrl: "https://live-collabcms-fe-demo.appa.pantheon.site", }, [Env.staging]: { addOnApiEndpoint: @@ -24,11 +26,13 @@ const config: { [key in Env]: Config } = { googleClientId: "142470191541-8o14j77pvagisc66s48kl4ub91f9c7b8.apps.googleusercontent.com", googleRedirectUri: "http://localhost:3030/oauth-redirect", + playgroundUrl: "https://multi-staging-collabcms-fe-demo.appa.pantheon.site", }, [Env.test]: { addOnApiEndpoint: "https://test-jest.comxyz/addOnApi", googleClientId: "test-google-com", googleRedirectUri: "http://localhost:3030/oauth-redirect", + playgroundUrl: "https://test-playground.site", }, }; diff --git a/packages/cli/src/lib/utils.ts b/packages/cli/src/lib/utils.ts new file mode 100644 index 00000000..ea7342ed --- /dev/null +++ b/packages/cli/src/lib/utils.ts @@ -0,0 +1,13 @@ +export function filterUndefinedProperties(obj: Record) { + const _obj = obj; + Object.keys(_obj).forEach((key) => + _obj[key] === undefined ? delete _obj[key] : {}, + ); + return _obj; +} +export function parameterize(obj: any, encode = true): string { + const func = encode ? encodeURIComponent : (s: string): string => s; + return Object.entries(obj) + .map(([k, v]) => `${func(k)}=${func(v)}`) + .join("&"); +} diff --git a/packages/cli/src/types/index.d.ts b/packages/cli/src/types/index.d.ts index 741bc02f..8f590a9f 100644 --- a/packages/cli/src/types/index.d.ts +++ b/packages/cli/src/types/index.d.ts @@ -1,5 +1,11 @@ declare type CliTemplateOptions = "nextjs" | "gatsby"; +declare type Article = { + id: string; + siteId: string; + title: string; +}; + declare type ApiKey = { id: string; keyMasked: string; @@ -10,6 +16,7 @@ declare type Site = { id: string; url: string; created?: number; + __isPlayground: boolean; }; declare type AuthDetails = {