Skip to content

Commit 7973547

Browse files
feat: introduce environment variable type-checking
1 parent 9526419 commit 7973547

File tree

9 files changed

+129
-37
lines changed

9 files changed

+129
-37
lines changed

src/framework/typescript/app/Application.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ class Application {
5151

5252
public constructor(
5353
public readonly rootPath: string,
54-
public readonly projectRootPath: string
54+
public readonly projectRootPath: string,
55+
public readonly version: string
5556
) {
5657
this.container = Container.getInstance();
5758
}

src/framework/typescript/commands/Context.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919

2020
import type { GuildConfig } from "@main/schemas/GuildConfigSchema";
21+
import type { SystemConfig } from "@main/schemas/SystemConfigSchema";
2122
import type {
2223
ChatInputCommandInteraction,
2324
ContextMenuCommandInteraction,
@@ -109,6 +110,10 @@ abstract class Context<T extends CommandMessage = CommandMessage> {
109110
return Application.current().service("configManager").config[this.guildId];
110111
}
111112

113+
public get systemConfig(): SystemConfig {
114+
return Application.current().service("configManager").systemConfig;
115+
}
116+
112117
public abstract get userId(): Snowflake;
113118
public abstract get user(): User;
114119

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { ZodSchema, z } from "zod";
2+
3+
class Environment {
4+
public static isProduction(): boolean {
5+
return process.env.NODE_ENV === "production" || process.env.NODE_ENV === "prod";
6+
}
7+
8+
public static isDevelopment(): boolean {
9+
return process.env.NODE_ENV === "development" || process.env.NODE_ENV === "dev";
10+
}
11+
12+
public static isTest(): boolean {
13+
return process.env.NODE_ENV === "test";
14+
}
15+
16+
public static isBun(): boolean {
17+
return !!process.isBun;
18+
}
19+
20+
public static variables(): NodeJS.ProcessEnv {
21+
return process.env;
22+
}
23+
24+
/**
25+
* Parse environment variables using a Zod schema.
26+
*
27+
* @param schema The Zod schema to use.
28+
* @returns The parsed environment variables.
29+
* @throws {z.ZodError} If the environment variables do not match the schema.
30+
*/
31+
public static parseVariables<T extends ZodSchema>(schema: T): z.infer<T> {
32+
return schema.parse(this.variables());
33+
}
34+
}
35+
36+
export default Environment;

src/main/typescript/core/DiscordKernel.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type { AnyConstructor } from "@framework/container/Container";
2121
import Container from "@framework/container/Container";
2222
import Kernel from "@framework/core/Kernel";
2323
import { Logger } from "@framework/log/Logger";
24+
import { createAxiosClient } from "@main/utils/axios";
2425
import metadata from "@root/package.json";
2526
import axios from "axios";
2627
import { spawn } from "child_process";
@@ -159,6 +160,8 @@ class DiscordKernel extends Kernel {
159160
key: binding.key
160161
});
161162
}
163+
164+
createAxiosClient(application);
162165
}
163166

164167
public getClient() {

src/main/typescript/env/env.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Environment from "@framework/env/Environment";
2+
import type { EnvironmentVariableRecord } from "@main/schemas/EnvironmentVariableSchema";
3+
import { EnvironmentVariableSchema } from "@main/schemas/EnvironmentVariableSchema";
4+
5+
export const env: EnvironmentVariableRecord = Environment.parseVariables(EnvironmentVariableSchema);

src/main/typescript/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,21 @@ import "module-alias/register";
2121
import "reflect-metadata";
2222

2323
import Application from "@framework/app/Application";
24+
import { version } from "@root/package.json";
2425
import path from "path";
2526
import DiscordKernel from "./core/DiscordKernel";
2627

2728
async function main() {
2829
Application.setupGlobals();
29-
const application = new Application(path.resolve(__dirname), path.resolve(__dirname, "../../.."));
30+
31+
const rootDirectoryPath = path.resolve(__dirname);
32+
const projectRootDirectoryPath = path.resolve(__dirname, "../../..");
33+
const application = new Application(
34+
rootDirectoryPath,
35+
projectRootDirectoryPath,
36+
process.env.SUDOBOT_VERSION ?? version
37+
);
38+
3039
await application.run(new DiscordKernel());
3140
}
3241

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { z } from "zod";
2+
3+
export const EnvironmentVariableSchema = z.object({
4+
TOKEN: z.string(),
5+
CLIENT_ID: z.string(),
6+
CLIENT_SECRET: z.string(),
7+
DB_URL: z.string(),
8+
JWT_SECRET: z.string(),
9+
JWT_ISSUER: z.string().default("SudoBot"),
10+
HOME_GUILD_ID: z.string(),
11+
SUDO_ENV: z.enum(["dev", "prod"]).optional(),
12+
NODE_ENV: z.enum(["dev", "prod", "development", "production", "test"]).default("production"),
13+
DEBUG: z.enum(["1", "0"]).optional(),
14+
BASE_SERVER_URL: z.string().optional(),
15+
DISCORD_OAUTH2_REDIRECT_URI: z.string().optional(),
16+
DISCORD_OAUTH2_RP_REDIRECT_URI: z.string().optional(),
17+
FRONTEND_URL: z.string().optional(),
18+
SUDO_PREFIX: z.string().optional(),
19+
ERROR_WEBHOOK_URL: z.string().optional(),
20+
BACKUP_CHANNEL_ID: z.string().optional(),
21+
FRONTEND_AUTH_KEY: z.string().optional(),
22+
CREDENTIAL_SERVER: z.string().optional(),
23+
PRIVATE_BOT_MODE: z.literal("true").optional(),
24+
EXTENSIONS_DIRECTORY: z.string().optional(),
25+
NO_GENERATE_CONFIG_SCHEMA: z.string().optional(),
26+
NSFWJS_MODEL_URL: z.string().optional(),
27+
NSFWJS_MODEL_IMAGE_SIZE: z.string().optional(),
28+
BACKUP_STORAGE: z.string().optional(),
29+
SUPPRESS_LOGS: z.string().optional(),
30+
CAT_API_TOKEN: z.string().optional(),
31+
DOG_API_TOKEN: z.string().optional(),
32+
HTTP_USER_AGENT: z
33+
.string()
34+
.optional()
35+
.nullable()
36+
.transform(value => (value === "null" ? null : value))
37+
});
38+
39+
export type EnvironmentVariableRecord = z.infer<typeof EnvironmentVariableSchema>;

src/main/typescript/types/global/env.d.ts

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,40 +17,10 @@
1717
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919

20-
namespace NodeJS {
21-
interface ProcessEnv {
22-
TOKEN: string;
23-
CLIENT_ID: string;
24-
HOME_GUILD_ID: string;
25-
DB_URL: string;
26-
JWT_SECRET: string;
27-
ENV?: "dev" | "prod";
28-
SUDO_ENV?: "dev" | "prod";
29-
NODE_ENV?: "dev" | "prod" | "development" | "production";
30-
DEBUG?: "1" | "0";
31-
CLIENT_SECRET: string;
32-
BASE_SERVER_URL: string;
33-
DISCORD_OAUTH2_REDIRECT_URI: string;
34-
DISCORD_OAUTH2_RP_REDIRECT_URI: string;
35-
FRONTEND_URL: string;
36-
SUDO_PREFIX?: string;
37-
ERROR_WEKHOOK_URL?: string;
38-
BACKUP_CHANNEL_ID?: string;
39-
JWT_ISSUER: string;
40-
JWT_SECRET: string;
41-
FRONTEND_AUTH_KEY: string;
42-
CREDENTIAL_SERVER?: string;
43-
PRIVATE_BOT_MODE?: string;
44-
GEMINI_API_KEY?: string;
45-
GEMINI_API_MODEL_CODE?: string;
46-
OPENAI_API_KEY?: string;
47-
OPENAI_MODEL_ID?: string;
48-
EXTENSIONS_DIRECTORY?: string;
49-
OPENAI_MODERATION?: string;
50-
NO_GENERATE_CONFIG_SCHEMA?: string;
51-
NSFWJS_MODEL_URL?: string;
52-
NSFWJS_MODEL_IMAGE_SIZE?: string;
53-
BACKUP_STORAGE?: string;
54-
SUPPRESS_LOGS?: string;
20+
import type { EnvironmentVariableRecord } from "@main/schemas/EnvironmentVariableSchema";
21+
22+
declare global {
23+
namespace NodeJS {
24+
interface ProcessEnv extends EnvironmentVariableRecord {}
5525
}
5626
}

src/main/typescript/utils/axios.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type Application from "@framework/app/Application";
2+
import { env } from "@main/env/env";
3+
import type { AxiosInstance } from "axios";
4+
import axios from "axios";
5+
6+
let _axiosClient: AxiosInstance;
7+
8+
export const createAxiosClient = (application: Application) => {
9+
const configUserAgent =
10+
env.HTTP_USER_AGENT === null
11+
? undefined
12+
: env.HTTP_USER_AGENT ?? `SudoBot/${application.version}`;
13+
14+
_axiosClient = axios.create({
15+
headers: {
16+
"Accept-Encoding": process.isBun ? "gzip" : undefined,
17+
"User-Agent": configUserAgent
18+
}
19+
});
20+
21+
return _axiosClient;
22+
};
23+
24+
export const getAxiosClient = () => _axiosClient;

0 commit comments

Comments
 (0)