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

feat(KNO-4367): Add pull email layout command #233

Merged
merged 45 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
243f3b0
feat(KNO-4367): add email layout pull, writer and helpers
francoborr Sep 25, 2023
e7ecf5a
feat(KNO-4367): pull one layout
francoborr Sep 26, 2023
6e71d95
feat(KNO-4367): add writer logic
francoborr Sep 26, 2023
11b6b21
feat(KNO-4367): add pull all email layout logic
francoborr Sep 26, 2023
6efb8ff
feat(KNO-4367): add final logic for pull all email layouts
francoborr Sep 26, 2023
fa5ec3f
feat(KNO-4367): final changes
francoborr Sep 27, 2023
d4c5af1
feat(KNO-4367): add tests
francoborr Sep 27, 2023
1487b19
chore(): fix lint
francoborr Sep 27, 2023
28aaea5
chore(): update comments
francoborr Sep 27, 2023
d9767fa
feat(KNO-4367): create layout.json when pulling email layouts
francoborr Sep 27, 2023
f534639
chore(KNO-4367): delete email_layout resource type
francoborr Sep 27, 2023
b2004e8
feat(KNO-4367): update run context loader
francoborr Sep 27, 2023
46c3bdf
chore(): fix lint
francoborr Sep 27, 2023
7f268ff
fix(KNO-4367): fix type check
francoborr Sep 27, 2023
ccd7116
chore(KNO-4367): fix lint
francoborr Sep 28, 2023
6ae7f8a
feat(KNO-4367): reference extracted files in layout.json
francoborr Sep 28, 2023
b1fe16c
chore(KNO-4367): move isLayoutDir check
francoborr Oct 2, 2023
01dd58c
feat(KNO-4367): add EmailLayoutDirContext
francoborr Oct 2, 2023
8345679
chore(): remove newline
francoborr Oct 2, 2023
3e17b71
feat(KNO-4367): add lsEmailLayoutJson
francoborr Oct 2, 2023
a2d3fb2
chore(): remove unused type
francoborr Oct 2, 2023
d59c983
chore(KNO-4367): fix multi line comment style
francoborr Oct 2, 2023
16dc22a
feat(KNO-4367): abstract readExtractedFileSync
francoborr Oct 3, 2023
e6a5393
feat(KNO-4367): read local layout when pulling
francoborr Oct 3, 2023
960b304
chore(KNO-4367): rename to remoteEmailLayout
francoborr Oct 3, 2023
74e65bf
feat(KNO-4367): make deep copy of remote layout
francoborr Oct 3, 2023
31de9e4
feat(KNO-4367): continue if layout does not have path
francoborr Oct 3, 2023
5e28104
feat(KNO-4367): add pruneLayoutsIndexDir
francoborr Oct 3, 2023
be90824
feat(KNO-4367): add email layout reader test
francoborr Oct 3, 2023
c6b4dc9
chore(KNO-4367): improve writer
francoborr Oct 3, 2023
d22033a
feat(KNO-4367): add writer test
francoborr Oct 3, 2023
ce8a5cc
feat(KNO-4367): add email layout path validations
francoborr Oct 3, 2023
e52610c
chore(KNO-4367): remove extra code
francoborr Oct 3, 2023
9d0f36f
chore(KNO-4367): remove comment
francoborr Oct 4, 2023
27d5be6
chore(): comment update
francoborr Oct 4, 2023
e47e680
chore(KNO-4367): update
francoborr Oct 4, 2023
29019c8
chore(KNO-4367): rename isLayoutDir
francoborr Oct 4, 2023
67a5038
chore(KNO-4367): remove newlines
francoborr Oct 4, 2023
447e001
feat(KNO-4367): improve joinExtractedFiles
francoborr Oct 4, 2023
d9c1595
chore(KNO-4367): update comment
francoborr Oct 4, 2023
77caaa0
chore(KNO-4367): update objPathParts
francoborr Oct 4, 2023
ce50327
feat(KNO-4367): abstract validateExtractedFilePath
francoborr Oct 4, 2023
0086276
chore(KNO-4367): add dot
francoborr Oct 4, 2023
7fe20bd
chore(KNO-4367): update
francoborr Oct 4, 2023
48743fa
feat(KNO-4367): improve validateExtractedFilePath
francoborr Oct 4, 2023
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
193 changes: 193 additions & 0 deletions src/commands/layout/pull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import * as path from "node:path";

import { Args, Flags } from "@oclif/core";

import * as ApiV1 from "@/lib/api-v1";
import BaseCommand from "@/lib/base-command";
import { ApiError } from "@/lib/helpers/error";
import * as CustomFlags from "@/lib/helpers/flag";
import { merge } from "@/lib/helpers/object";
import { MAX_PAGINATION_LIMIT, PageInfo } from "@/lib/helpers/page";
import {
formatErrorRespMessage,
isSuccessResp,
withSpinner,
} from "@/lib/helpers/request";
import { promptToConfirm, spinner } from "@/lib/helpers/ux";
import * as EmailLayout from "@/lib/marshal/email-layout";
import { WithAnnotation } from "@/lib/marshal/shared/types";
import {
ensureResourceDirForTarget,
LayoutDirContext,
ResourceTarget,
} from "@/lib/run-context";

export default class EmailLayoutPull extends BaseCommand<
typeof EmailLayoutPull
> {
static aliases = ["email-layout:pull", "email_layout:pull"];

static summary =
"Pull one or more email layouts from an environment into a local file system.";

static flags = {
environment: Flags.string({
default: "development",
summary: "The environment to use.",
}),
all: Flags.boolean({
summary:
"Whether to pull all email layouts from the specified environment.",
}),
"layouts-dir": CustomFlags.dirPath({
summary: "The target directory path to pull all email layouts into.",
dependsOn: ["all"],
aliases: ["email-layouts-dir"],
}),
"hide-uncommitted-changes": Flags.boolean({
summary: "Hide any uncommitted changes.",
}),
force: Flags.boolean({
summary: "Remove the confirmation prompt.",
}),
};

static args = {
emailLayoutKey: Args.string({
required: false,
}),
};

async run(): Promise<void> {
const { args, flags } = this.props;

if (flags.all && args.emailLayoutKey) {
return this.error(
`emailLayoutKey arg \`${args.emailLayoutKey}\` cannot also be provided when using --all`,
);
}

return flags.all ? this.pullAllEmailLayouts() : this.pullOneEmailLayout();
}

// Pull one email layout
async pullOneEmailLayout(): Promise<void> {
const { flags } = this.props;

const dirContext = await this.getEmailLayoutDirContext();

if (dirContext.exists) {
this.log(`‣ Found \`${dirContext.key}\` at ${dirContext.abspath}`);
} else {
const prompt = `Create a new email layout directory \`${dirContext.key}\` at ${dirContext.abspath}?`;
const input = flags.force || (await promptToConfirm(prompt));
if (!input) return;
}

const resp = await withSpinner<ApiV1.GetEmailLayoutResp<WithAnnotation>>(
() => {
const props = merge(this.props, {
args: { emailLayoutKey: dirContext.key },
flags: { annotate: true },
});
return this.apiV1.getEmailLayout(props);
},
);

await EmailLayout.writeEmailLayoutDirFromData(dirContext, resp.data);

const action = dirContext.exists ? "updated" : "created";
this.log(
`‣ Successfully ${action} \`${dirContext.key}\` at ${dirContext.abspath}`,
);
}

// Pull all email layouts
async pullAllEmailLayouts(): Promise<void> {
const { flags } = this.props;

const defaultToCwd = { abspath: this.runContext.cwd, exists: true };
const targetDirCtx = flags["layouts-dir"] || defaultToCwd;

const prompt = targetDirCtx.exists
? `Pull latest layouts into ${targetDirCtx.abspath}?\n This will overwrite the contents of this directory.`
: `Create a new layouts directory at ${targetDirCtx.abspath}?`;

const input = flags.force || (await promptToConfirm(prompt));
if (!input) return;

spinner.start(`‣ Loading`);

const emailLayouts = await this.listAllEmailLayouts();

await EmailLayout.writeEmailLayoutIndexDir(targetDirCtx, emailLayouts);
spinner.stop();

const action = targetDirCtx.exists ? "updated" : "created";
this.log(
`‣ Successfully ${action} the layouts directory at ${targetDirCtx.abspath}`,
);
}

async listAllEmailLayouts(
pageParams: Partial<PageInfo> = {},
emailLayoutsFetchedSoFar: EmailLayout.EmailLayoutData<WithAnnotation>[] = [],
): Promise<EmailLayout.EmailLayoutData<WithAnnotation>[]> {
const props = merge(this.props, {
flags: {
...pageParams,
annotate: true,
limit: MAX_PAGINATION_LIMIT,
},
});

const resp = await this.apiV1.listEmailLayouts<WithAnnotation>(props);
if (!isSuccessResp(resp)) {
const message = formatErrorRespMessage(resp);
this.error(new ApiError(message));
}

const { entries, page_info: pageInfo } = resp.data;
const emailLayouts = [...emailLayoutsFetchedSoFar, ...entries];

return pageInfo.after
? this.listAllEmailLayouts({ after: pageInfo.after }, emailLayouts)
: emailLayouts;
}

async getEmailLayoutDirContext(): Promise<LayoutDirContext> {
const { emailLayoutKey } = this.props.args;
const { resourceDir, cwd: runCwd } = this.runContext;

// Inside an existing resource dir, use it if valid for the target email layout.
if (resourceDir) {
const target: ResourceTarget = {
commandId: BaseCommand.id,
type: "layout",
key: emailLayoutKey,
};

return ensureResourceDirForTarget(
resourceDir,
target,
) as LayoutDirContext;
}

// Not inside any existing email layout directory, which means either create a
// new email layout directory in the cwd, or update it if there is one already.
if (emailLayoutKey) {
const dirPath = path.resolve(runCwd, emailLayoutKey);
const exists = await EmailLayout.isEmailLayoutDir(dirPath);

return {
type: "layout",
key: emailLayoutKey,
abspath: dirPath,
exists,
};
}

// Not in any email layout directory, nor a email layout key arg was given so error.
return this.error("Missing 1 required arg:\nemailLayoutKey");
}
}
31 changes: 31 additions & 0 deletions src/lib/marshal/email-layout/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as path from "node:path";

import * as fs from "fs-extra";

export const LAYOUT_JSON = "layout.json";

export type EmailLayoutFileContext = {
key: string;
abspath: string;
exists: boolean;
};

/*
* Evaluates whether the given directory path is an email layout directory, by
* checking for the presence of a `layout.json` file.
*/
export const isEmailLayoutDir = async (dirPath: string): Promise<boolean> =>
Boolean(await isEmailLayoutJson(dirPath));

/*
* Check for `layout.json` file and return the file path if present.
*/

export const isEmailLayoutJson = async (
dirPath: string,
): Promise<string | undefined> => {
const emailLayoutJsonPath = path.resolve(dirPath, LAYOUT_JSON);

const exists = await fs.pathExists(emailLayoutJsonPath);
return exists ? emailLayoutJsonPath : undefined;
};
2 changes: 2 additions & 0 deletions src/lib/marshal/email-layout/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from "./helpers";
export * from "./types";
export * from "./writer";
Loading