chore(config): centralize feature settings

This commit is contained in:
2026-08-09 21:20:19 +07:00 Unverified
parent 26c0b5af18
commit 950d3d4afe
22 changed files with 238 additions and 70 deletions
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, test } from "bun:test";
import {
optionalEnvironmentValue,
readFeatureEnvironment,
requiredEnvironmentUrl,
requiredEnvironmentValue,
} from "@/lib/config/environment";
describe("feature environment configuration", () => {
test("returns trimmed required and optional values", () => {
const environment = { REQUIRED: " value ", OPTIONAL: " optional " };
expect(requiredEnvironmentValue("REQUIRED", environment)).toBe("value");
expect(optionalEnvironmentValue("OPTIONAL", environment)).toBe("optional");
expect(optionalEnvironmentValue("MISSING", environment)).toBeNull();
});
test("reports the feature and missing variable", () => {
expect(() =>
readFeatureEnvironment("Discord", ["TOKEN", "GUILD"] as const, {
TOKEN: "token",
}),
).toThrow("Discord is not configured: Missing required environment variable: GUILD");
});
test("accepts absolute URLs and removes a trailing slash", () => {
expect(requiredEnvironmentUrl("BASE_URL", { BASE_URL: "https://example.com/" }))
.toBe("https://example.com");
expect(() => requiredEnvironmentUrl("BASE_URL", { BASE_URL: "/relative" }))
.toThrow("must be an absolute URL");
});
});
+52
View File
@@ -0,0 +1,52 @@
import { z } from "zod";
const nonEmptyString = z.string().trim().min(1);
const urlString = z.url();
export type Environment = Record<string, string | undefined>;
export function requiredEnvironmentValue(
name: string,
environment: Environment = process.env,
) {
const parsed = nonEmptyString.safeParse(environment[name]);
if (!parsed.success) {
throw new Error(`Missing required environment variable: ${name}`);
}
return parsed.data;
}
export function optionalEnvironmentValue(
name: string,
environment: Environment = process.env,
) {
const parsed = nonEmptyString.safeParse(environment[name]);
return parsed.success ? parsed.data : null;
}
export function requiredEnvironmentUrl(
name: string,
environment: Environment = process.env,
) {
const value = requiredEnvironmentValue(name, environment);
const parsed = urlString.safeParse(value);
if (!parsed.success) {
throw new Error(`Environment variable ${name} must be an absolute URL`);
}
return parsed.data.replace(/\/$/, "");
}
export function readFeatureEnvironment<Names extends readonly string[]>(
feature: string,
names: Names,
environment: Environment = process.env,
): { [Name in Names[number]]: string } {
try {
return Object.fromEntries(
names.map((name) => [name, requiredEnvironmentValue(name, environment)]),
) as { [Name in Names[number]]: string };
} catch (error) {
const message = error instanceof Error ? error.message : "Invalid configuration";
throw new Error(`${feature} is not configured: ${message}`, { cause: error });
}
}
+77
View File
@@ -0,0 +1,77 @@
import "server-only";
import {
optionalEnvironmentValue,
readFeatureEnvironment,
requiredEnvironmentUrl,
} from "@/lib/config/environment";
export function getCanonicalUrl() {
if (optionalEnvironmentValue("BASE_URL")) {
return requiredEnvironmentUrl("BASE_URL");
}
return requiredEnvironmentUrl("NEXTAUTH_URL");
}
export function getDiscordServerConfig() {
const config = readFeatureEnvironment("Discord", [
"DISCORD_BOT_TOKEN",
"DISCORD_GUILD_ID",
] as const);
return {
token: config.DISCORD_BOT_TOKEN,
guildId: config.DISCORD_GUILD_ID,
};
}
export function getYoutubeFollowerConfig() {
const config = readFeatureEnvironment("YouTube follower counts", [
"YOUTUBE_API_KEY",
"YOUTUBE_CHANNEL_ID",
] as const);
return { apiKey: config.YOUTUBE_API_KEY, channelId: config.YOUTUBE_CHANNEL_ID };
}
export function getRobloxFollowerConfig() {
return {
userId: readFeatureEnvironment("Roblox follower counts", [
"ROBLOX_USER_ID",
] as const).ROBLOX_USER_ID,
};
}
export function getTiktokApiConfig() {
const accessToken = optionalEnvironmentValue("TIKTOK_ACCESS_TOKEN");
if (accessToken) {
return {
clientKey: null,
clientSecret: null,
refreshToken: null,
accessToken,
};
}
const config = readFeatureEnvironment("TikTok", [
"TIKTOK_CLIENT_KEY",
"TIKTOK_CLIENT_SECRET",
"TIKTOK_REFRESH_TOKEN",
] as const);
return {
clientKey: config.TIKTOK_CLIENT_KEY,
clientSecret: config.TIKTOK_CLIENT_SECRET,
refreshToken: config.TIKTOK_REFRESH_TOKEN,
accessToken: null,
};
}
export function getYoutubePublishingConfig() {
const config = readFeatureEnvironment("YouTube publishing", [
"YOUTUBE_CLIENT_ID",
"YOUTUBE_CLIENT_SECRET",
"YOUTUBE_REFRESH_TOKEN",
] as const);
return {
clientId: config.YOUTUBE_CLIENT_ID,
clientSecret: config.YOUTUBE_CLIENT_SECRET,
refreshToken: config.YOUTUBE_REFRESH_TOKEN,
};
}