Skip to content

Commit

Permalink
🏗 build(cli): extract args before running build cmd (#196)
Browse files Browse the repository at this point in the history
* refactor: extract cli args before running cmd

* feat: validate pckgs before building

* chore: update delete cmd description

* feat: support force option when building node pckgs

* refactor(cli): convert CLI to class and use zod for validation

* feat(cli): add clientId as build option

* fix(cli): validate packages after they're provided

* refactor(cli): move validation from util to Cli class

this improves readability, because the class has access to the 'options', which means we don't have to keep passing that arg to the util functions

* refactor(cli): extract parsing  and validation into separate class

this makes the division of responsibilities more clear: the Validator parses cli args and validates their inputs against our types. The CLI is then free to accept the parsed args and simply trigger the provided commands

* build(deps): add zod to backend
  • Loading branch information
tyler-dane authored Jan 7, 2025
1 parent 1423ed4 commit 67db6a0
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 111 deletions.
3 changes: 2 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"saslprep": "^1.0.3",
"socket.io": "^4.7.5",
"supertokens-node": "^20.0.5",
"tslib": "^2.4.0"
"tslib": "^2.4.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@shelf/jest-mongodb": "^4.1.4",
Expand Down
110 changes: 60 additions & 50 deletions packages/scripts/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,66 +8,76 @@ import { Command } from "commander";
import { runBuild } from "./commands/build";
import { ALL_PACKAGES, CATEGORY_VM } from "./common/cli.constants";
import { startDeleteFlow } from "./commands/delete";
import { log } from "./common/cli.utils";
import { CliValidator } from "./cli.validator";

const runScript = async () => {
const exitHelpfully = (msg?: string) => {
msg && log.error(msg);
console.log(program.helpInformation());
process.exit(1);
};
class CompassCli {
private program: Command;
private validator: CliValidator;

const program = new Command();
program.option(
`-e, --environment [${CATEGORY_VM.STAG}|${CATEGORY_VM.PROD}]`,
"specify environment"
);
program.option("-f, --force", "forces operation, no cautionary prompts");
program.option(
"-u, --user [id|email]",
"specifies which user to run script for"
);

program
.command("build")
.description("build compass package(s)")
.argument(
`[${ALL_PACKAGES.join("|")}]`,
"package(s) to build, separated by comma"
)
.option("--skip-env", "skips copying env files to build");
constructor(args: string[]) {
this.program = this._createProgram();
this.validator = new CliValidator(this.program);
this.program.parse(args);
}

program
.command("delete")
.description("deletes users data from compass database");
public async run() {
const options = this.validator.getCliOptions();
const { force, user } = options;
const cmd = this.program.args[0];

program.parse(process.argv);
switch (true) {
case cmd === "build": {
await this.validator.validateBuild(options);
await runBuild(options);
break;
}
case cmd === "delete": {
this.validator.validateDelete(options);
await startDeleteFlow(user as string, force);
break;
}
default:
this.validator.exitHelpfully(
"root",
`${cmd as string} is not a supported cmd`
);
}
}

const options = program.opts();
const cmd = program.args[0];
private _createProgram(): Command {
const program = new Command();

switch (true) {
case cmd === "build": {
await runBuild(options);
break;
}
case cmd === "delete": {
const force = options["force"] as boolean;
const user = options["user"] as string;
program.option("-f, --force", "force operation, no cautionary prompts");

if (!user || typeof user !== "string") {
exitHelpfully("You must supply a user");
}
program
.command("build")
.description("build compass package")
.argument(
`[${ALL_PACKAGES.join(" | ")}]`,
"package to build (only provide 1)"
)
.option(
"-c, --clientId <clientId>",
"google client id to inject into build"
)
.option(
`-e, --environment [${CATEGORY_VM.STAG} | ${CATEGORY_VM.PROD}]`,
"specify environment"
);

await startDeleteFlow(user, force);
break;
}
default:
exitHelpfully("Unsupported cmd");
program
.command("delete")
.description("delete user data from compass database")
.option(
"-u, --user [id | email]",
"specify which user to run script for"
);
return program;
}
};
}

runScript().catch((err) => {
const cli = new CompassCli(process.argv);
cli.run().catch((err) => {
console.log(err);
process.exit(1);
});
168 changes: 168 additions & 0 deletions packages/scripts/src/cli.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { Command } from "commander";

import { ALL_PACKAGES } from "./common/cli.constants";
import {
Options_Cli,
Options_Cli_Build,
Options_Cli_Delete,
Schema_Options_Cli_Build,
Schema_Options_Cli_Delete,
Schema_Options_Cli_Root,
} from "./common/cli.types";
import { getPckgsTo, log } from "./common/cli.utils";

export class CliValidator {
private program: Command;

constructor(program: Command) {
this.program = program;
}

public exitHelpfully(cmd: "root" | "build" | "delete", msg?: string) {
msg && log.error(msg);

if (cmd === "root") {
console.log(this.program.helpInformation());
} else {
const command = this.program.commands.find(
(c) => c.name() === cmd
) as Command;
console.log(command.helpInformation());
}

process.exit(1);
}

public getCliOptions(): Options_Cli {
const options = this._mergeOptions();
const validOptions = this._validateOptions(options);

return validOptions;
}

public async validateBuild(options: Options_Cli) {
if (!options.packages) {
options.packages = await getPckgsTo("build");
}

const unsupportedPackages = options.packages.filter(
(pkg) => !ALL_PACKAGES.includes(pkg)
);
if (unsupportedPackages.length > 0) {
this.exitHelpfully(
"build",
`One or more of these packages isn't supported: ${unsupportedPackages.toString()}`
);
}
}

public validateDelete(options: Options_Cli) {
const { user } = options;
if (!user || typeof user !== "string") {
this.exitHelpfully("delete", "You must supply a user");
}
}

private _getBuildOptions() {
const buildOpts: Options_Cli_Build = {};

const buildCmd = this.program.commands.find(
(cmd) => cmd.name() === "build"
);
if (buildCmd) {
const packages = this.program.args[1]?.split(",");
if (packages) {
buildOpts.packages = packages;
}

const environment = buildCmd?.opts()[
"environment"
] as Options_Cli_Build["environment"];
if (environment) {
buildOpts.environment = environment;
}

const clientId = buildCmd?.opts()[
"clientId"
] as Options_Cli_Build["clientId"];
if (clientId) {
buildOpts.clientId = clientId;
}
}
return buildOpts;
}

private _getDeleteOptions() {
const deleteOpts: Options_Cli_Delete = {};

const deleteCmd = this.program.commands.find(
(cmd) => cmd.name() === "delete"
);
if (deleteCmd) {
const user = deleteCmd?.opts()["user"] as Options_Cli["user"];
if (user) {
deleteOpts.user = user;
}
}

return deleteOpts;
}

private _mergeOptions = (): Options_Cli => {
const _options = this.program.opts();
let options: Options_Cli = {
..._options,
force: _options["force"] === true,
};

const buildOptions = this._getBuildOptions();
if (Object.keys(buildOptions).length > 0) {
options = {
...options,
...buildOptions,
};
}

const deleteOptions = this._getDeleteOptions();
if (Object.keys(deleteOptions).length > 0) {
options = {
...options,
...deleteOptions,
};
}

return options;
};

private _validateOptions(options: Options_Cli) {
const { data: rootData, error: rootError } =
Schema_Options_Cli_Root.safeParse(options);
if (rootError) {
this.exitHelpfully(
"root",
`Invalid CLI options: ${rootError.toString()}`
);
}

const { data: buildData, error: buildError } =
Schema_Options_Cli_Build.safeParse(options);
if (buildError) {
this.exitHelpfully(
"build",
`Invalid build options: ${buildError.toString()}`
);
}

const { data: deleteData, error: deleteError } =
Schema_Options_Cli_Delete.safeParse(options);
if (deleteError) {
this.exitHelpfully(
"delete",
`Invalid delete options: ${deleteError.toString()}`
);
}

const data: Options_Cli = { ...rootData, ...buildData, ...deleteData };
return data;
}
}
Loading

0 comments on commit 67db6a0

Please sign in to comment.