Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Od/cli/preview command #63

Merged
merged 7 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions packages/cli/src/cli/commands/documents.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
},
) {
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<GeneratePreviewParam>(
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));
},
);
52 changes: 43 additions & 9 deletions packages/cli/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -141,7 +142,7 @@ yargs(hideBin(process.argv))
"Revokes token for a given id.",
(yargs) => {
yargs.positional("<id>", {
describe: "ID of the token which you want to revoke",
describe: "ID of the token which you want to revoke.",
demandOption: true,
type: "string",
});
Expand Down Expand Up @@ -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,
});
Expand All @@ -204,12 +205,12 @@ yargs(hideBin(process.argv))
)
.command(
"configure <id> [options]",
"Configure properties for a given site",
"Configure properties for a given site.",
(yargs) => {
yargs
.strictCommands()
.positional("<id>", {
describe: "ID of the site which you want to configure",
describe: "ID of the site which you want to configure.",
demandOption: true,
type: "string",
})
Expand Down Expand Up @@ -245,24 +246,25 @@ yargs(hideBin(process.argv))
)
.command(
"webhooks <cmd> [options]",
"Manage webhooks for a given site",
"Manage webhooks for a given site.",
(yargs) => {
yargs
.strictCommands()
.demandCommand()
.command(
"history <id>",
"View webhook event delivery logs for a given site",
"View webhook event delivery logs for a given site.",
(yargs) => {
yargs
.strictCommands()
.positional("<id>", {
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,
Expand All @@ -281,6 +283,38 @@ yargs(hideBin(process.argv))
// noop
},
)
.command(
"document <cmd> [options]",
"Enables you to manage documents for a PCC Project.",
(yargs) => {
yargs
.strictCommands()
.demandCommand()
.command(
"preview <id>",
"Generates preview link for a given document ID.",
(yargs) => {
yargs
.strictCommands()
.positional("<id>", {
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.",
Expand Down
39 changes: 39 additions & 0 deletions packages/cli/src/lib/addonApiHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -22,6 +23,32 @@ class AddOnApiHelper {
return resp.data as Credentials;
}

static async getDocument(documentId: string): Promise<Article> {
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<string> {
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<string> {
const authDetails = await getLocalAuthDetails();
if (!authDetails) throw new UserNotLoggedIn();
Expand Down Expand Up @@ -98,6 +125,18 @@ class AddOnApiHelper {

return resp.data as Site[];
}
static async getSite(siteId: string): Promise<Site> {
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<void> {
const authDetails = await getLocalAuthDetails();
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type Config = {
addOnApiEndpoint: string;
googleClientId: string;
googleRedirectUri: string;
playgroundUrl: string;
};
const ENV: Env = (process.env.NODE_ENV as Env) || "production";

Expand All @@ -17,18 +18,21 @@ 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:
"https://us-central1-pantheon-content-cloud-staging.cloudfunctions.net/addOnApi",
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",
},
};

Expand Down
13 changes: 13 additions & 0 deletions packages/cli/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function filterUndefinedProperties(obj: Record<string, any>) {

Check warning on line 1 in packages/cli/src/lib/utils.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
const _obj = obj;
Object.keys(_obj).forEach((key) =>
_obj[key] === undefined ? delete _obj[key] : {},
);
return _obj;
}
export function parameterize(obj: any, encode = true): string {

Check warning on line 8 in packages/cli/src/lib/utils.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
const func = encode ? encodeURIComponent : (s: string): string => s;
return Object.entries<string>(obj)
.map(([k, v]) => `${func(k)}=${func(v)}`)
.join("&");
}
7 changes: 7 additions & 0 deletions packages/cli/src/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,6 +16,7 @@ declare type Site = {
id: string;
url: string;
created?: number;
__isPlayground: boolean;
};

declare type AuthDetails = {
Expand Down
Loading