Skip to content

Commit 67db6a0

Browse files
authored
🏗 build(cli): extract args before running build cmd (#196)
* 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
1 parent 1423ed4 commit 67db6a0

File tree

6 files changed

+301
-111
lines changed

6 files changed

+301
-111
lines changed

packages/backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"saslprep": "^1.0.3",
2222
"socket.io": "^4.7.5",
2323
"supertokens-node": "^20.0.5",
24-
"tslib": "^2.4.0"
24+
"tslib": "^2.4.0",
25+
"zod": "^3.24.1"
2526
},
2627
"devDependencies": {
2728
"@shelf/jest-mongodb": "^4.1.4",

packages/scripts/src/cli.ts

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,66 +8,76 @@ import { Command } from "commander";
88
import { runBuild } from "./commands/build";
99
import { ALL_PACKAGES, CATEGORY_VM } from "./common/cli.constants";
1010
import { startDeleteFlow } from "./commands/delete";
11-
import { log } from "./common/cli.utils";
11+
import { CliValidator } from "./cli.validator";
1212

13-
const runScript = async () => {
14-
const exitHelpfully = (msg?: string) => {
15-
msg && log.error(msg);
16-
console.log(program.helpInformation());
17-
process.exit(1);
18-
};
13+
class CompassCli {
14+
private program: Command;
15+
private validator: CliValidator;
1916

20-
const program = new Command();
21-
program.option(
22-
`-e, --environment [${CATEGORY_VM.STAG}|${CATEGORY_VM.PROD}]`,
23-
"specify environment"
24-
);
25-
program.option("-f, --force", "forces operation, no cautionary prompts");
26-
program.option(
27-
"-u, --user [id|email]",
28-
"specifies which user to run script for"
29-
);
30-
31-
program
32-
.command("build")
33-
.description("build compass package(s)")
34-
.argument(
35-
`[${ALL_PACKAGES.join("|")}]`,
36-
"package(s) to build, separated by comma"
37-
)
38-
.option("--skip-env", "skips copying env files to build");
17+
constructor(args: string[]) {
18+
this.program = this._createProgram();
19+
this.validator = new CliValidator(this.program);
20+
this.program.parse(args);
21+
}
3922

40-
program
41-
.command("delete")
42-
.description("deletes users data from compass database");
23+
public async run() {
24+
const options = this.validator.getCliOptions();
25+
const { force, user } = options;
26+
const cmd = this.program.args[0];
4327

44-
program.parse(process.argv);
28+
switch (true) {
29+
case cmd === "build": {
30+
await this.validator.validateBuild(options);
31+
await runBuild(options);
32+
break;
33+
}
34+
case cmd === "delete": {
35+
this.validator.validateDelete(options);
36+
await startDeleteFlow(user as string, force);
37+
break;
38+
}
39+
default:
40+
this.validator.exitHelpfully(
41+
"root",
42+
`${cmd as string} is not a supported cmd`
43+
);
44+
}
45+
}
4546

46-
const options = program.opts();
47-
const cmd = program.args[0];
47+
private _createProgram(): Command {
48+
const program = new Command();
4849

49-
switch (true) {
50-
case cmd === "build": {
51-
await runBuild(options);
52-
break;
53-
}
54-
case cmd === "delete": {
55-
const force = options["force"] as boolean;
56-
const user = options["user"] as string;
50+
program.option("-f, --force", "force operation, no cautionary prompts");
5751

58-
if (!user || typeof user !== "string") {
59-
exitHelpfully("You must supply a user");
60-
}
52+
program
53+
.command("build")
54+
.description("build compass package")
55+
.argument(
56+
`[${ALL_PACKAGES.join(" | ")}]`,
57+
"package to build (only provide 1)"
58+
)
59+
.option(
60+
"-c, --clientId <clientId>",
61+
"google client id to inject into build"
62+
)
63+
.option(
64+
`-e, --environment [${CATEGORY_VM.STAG} | ${CATEGORY_VM.PROD}]`,
65+
"specify environment"
66+
);
6167

62-
await startDeleteFlow(user, force);
63-
break;
64-
}
65-
default:
66-
exitHelpfully("Unsupported cmd");
68+
program
69+
.command("delete")
70+
.description("delete user data from compass database")
71+
.option(
72+
"-u, --user [id | email]",
73+
"specify which user to run script for"
74+
);
75+
return program;
6776
}
68-
};
77+
}
6978

70-
runScript().catch((err) => {
79+
const cli = new CompassCli(process.argv);
80+
cli.run().catch((err) => {
7181
console.log(err);
7282
process.exit(1);
7383
});
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { Command } from "commander";
2+
3+
import { ALL_PACKAGES } from "./common/cli.constants";
4+
import {
5+
Options_Cli,
6+
Options_Cli_Build,
7+
Options_Cli_Delete,
8+
Schema_Options_Cli_Build,
9+
Schema_Options_Cli_Delete,
10+
Schema_Options_Cli_Root,
11+
} from "./common/cli.types";
12+
import { getPckgsTo, log } from "./common/cli.utils";
13+
14+
export class CliValidator {
15+
private program: Command;
16+
17+
constructor(program: Command) {
18+
this.program = program;
19+
}
20+
21+
public exitHelpfully(cmd: "root" | "build" | "delete", msg?: string) {
22+
msg && log.error(msg);
23+
24+
if (cmd === "root") {
25+
console.log(this.program.helpInformation());
26+
} else {
27+
const command = this.program.commands.find(
28+
(c) => c.name() === cmd
29+
) as Command;
30+
console.log(command.helpInformation());
31+
}
32+
33+
process.exit(1);
34+
}
35+
36+
public getCliOptions(): Options_Cli {
37+
const options = this._mergeOptions();
38+
const validOptions = this._validateOptions(options);
39+
40+
return validOptions;
41+
}
42+
43+
public async validateBuild(options: Options_Cli) {
44+
if (!options.packages) {
45+
options.packages = await getPckgsTo("build");
46+
}
47+
48+
const unsupportedPackages = options.packages.filter(
49+
(pkg) => !ALL_PACKAGES.includes(pkg)
50+
);
51+
if (unsupportedPackages.length > 0) {
52+
this.exitHelpfully(
53+
"build",
54+
`One or more of these packages isn't supported: ${unsupportedPackages.toString()}`
55+
);
56+
}
57+
}
58+
59+
public validateDelete(options: Options_Cli) {
60+
const { user } = options;
61+
if (!user || typeof user !== "string") {
62+
this.exitHelpfully("delete", "You must supply a user");
63+
}
64+
}
65+
66+
private _getBuildOptions() {
67+
const buildOpts: Options_Cli_Build = {};
68+
69+
const buildCmd = this.program.commands.find(
70+
(cmd) => cmd.name() === "build"
71+
);
72+
if (buildCmd) {
73+
const packages = this.program.args[1]?.split(",");
74+
if (packages) {
75+
buildOpts.packages = packages;
76+
}
77+
78+
const environment = buildCmd?.opts()[
79+
"environment"
80+
] as Options_Cli_Build["environment"];
81+
if (environment) {
82+
buildOpts.environment = environment;
83+
}
84+
85+
const clientId = buildCmd?.opts()[
86+
"clientId"
87+
] as Options_Cli_Build["clientId"];
88+
if (clientId) {
89+
buildOpts.clientId = clientId;
90+
}
91+
}
92+
return buildOpts;
93+
}
94+
95+
private _getDeleteOptions() {
96+
const deleteOpts: Options_Cli_Delete = {};
97+
98+
const deleteCmd = this.program.commands.find(
99+
(cmd) => cmd.name() === "delete"
100+
);
101+
if (deleteCmd) {
102+
const user = deleteCmd?.opts()["user"] as Options_Cli["user"];
103+
if (user) {
104+
deleteOpts.user = user;
105+
}
106+
}
107+
108+
return deleteOpts;
109+
}
110+
111+
private _mergeOptions = (): Options_Cli => {
112+
const _options = this.program.opts();
113+
let options: Options_Cli = {
114+
..._options,
115+
force: _options["force"] === true,
116+
};
117+
118+
const buildOptions = this._getBuildOptions();
119+
if (Object.keys(buildOptions).length > 0) {
120+
options = {
121+
...options,
122+
...buildOptions,
123+
};
124+
}
125+
126+
const deleteOptions = this._getDeleteOptions();
127+
if (Object.keys(deleteOptions).length > 0) {
128+
options = {
129+
...options,
130+
...deleteOptions,
131+
};
132+
}
133+
134+
return options;
135+
};
136+
137+
private _validateOptions(options: Options_Cli) {
138+
const { data: rootData, error: rootError } =
139+
Schema_Options_Cli_Root.safeParse(options);
140+
if (rootError) {
141+
this.exitHelpfully(
142+
"root",
143+
`Invalid CLI options: ${rootError.toString()}`
144+
);
145+
}
146+
147+
const { data: buildData, error: buildError } =
148+
Schema_Options_Cli_Build.safeParse(options);
149+
if (buildError) {
150+
this.exitHelpfully(
151+
"build",
152+
`Invalid build options: ${buildError.toString()}`
153+
);
154+
}
155+
156+
const { data: deleteData, error: deleteError } =
157+
Schema_Options_Cli_Delete.safeParse(options);
158+
if (deleteError) {
159+
this.exitHelpfully(
160+
"delete",
161+
`Invalid delete options: ${deleteError.toString()}`
162+
);
163+
}
164+
165+
const data: Options_Cli = { ...rootData, ...buildData, ...deleteData };
166+
return data;
167+
}
168+
}

0 commit comments

Comments
 (0)