feat: stabilize API startup and lifecycle management
- Refactor server initialization to separate concerns and improve error handling. - Implement centralized environment validation using Zod. - Introduce database, Redis, and queue lifecycle management. - Add health check endpoints for liveness and readiness. - Enhance error handling middleware for better response structure. - Implement rate limiting for API endpoints. - Add request ID middleware for traceability. - Create Sequelize CLI configuration and baseline migration for schema management. - Establish CI workflow with Gitea for testing and syntax checks. - Document foundational changes and migration strategy in PHASE_0_FOUNDATION_STABILIZATION.md. - Add Docker Compose configuration for local development and testing. - Implement unit and integration tests for critical functionality.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
const { z } = require("zod");
|
||||
|
||||
const booleanString = z.enum(["true", "false"]).default("false").transform((value) => value === "true");
|
||||
|
||||
const envSchema = z.object({
|
||||
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
||||
PORT: z.coerce.number().int().min(1).max(65535).default(3070),
|
||||
DB_HOST: z.string().min(1),
|
||||
DB_PORT: z.coerce.number().int().min(1).max(65535).default(3306),
|
||||
DB_NAME: z.string().min(1),
|
||||
DB_USER: z.string().min(1),
|
||||
DB_PASSWORD: z.string(),
|
||||
JWT_SECRET: z.string().min(32, "JWT_SECRET must contain at least 32 characters"),
|
||||
REFRESH_TOKEN_SECRET: z.string().min(32, "REFRESH_TOKEN_SECRET must contain at least 32 characters"),
|
||||
REDIS_HOST: z.string().min(1),
|
||||
REDIS_PORT: z.coerce.number().int().min(1).max(65535).default(6379),
|
||||
REDIS_PASSWORD: z.string().optional(),
|
||||
FRONTEND_URL: z.string().url(),
|
||||
TRUST_PROXY: z.coerce.number().int().min(0).max(10).default(0),
|
||||
JSON_BODY_LIMIT: z.string().default("1mb"),
|
||||
API_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(900000),
|
||||
API_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(300),
|
||||
SENSITIVE_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(900000),
|
||||
SENSITIVE_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(20),
|
||||
RUN_CRON: booleanString,
|
||||
ENABLE_MAIL: booleanString,
|
||||
ENABLE_S3: booleanString,
|
||||
SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().positive().default(10000),
|
||||
CACHE: booleanString,
|
||||
MAIL_HOST: z.string().min(1).optional(), MAIL_PORT: z.coerce.number().int().positive().optional(),
|
||||
MAIL_SECURE: z.enum(["true", "false"]).optional(), MAIL_USER: z.string().optional(),
|
||||
MAIL_PASS: z.string().optional(), MAIL_FROM: z.string().optional(),
|
||||
AWS_REGION: z.string().optional(), AWS_ACCESS_KEY_ID: z.string().optional(),
|
||||
AWS_SECRET_ACCESS_KEY: z.string().optional(), AWS_S3_BUCKET_NAME: z.string().optional(),
|
||||
DOCS_USER: z.string().optional(), DOCS_PASS: z.string().optional(),
|
||||
}).superRefine((env, context) => {
|
||||
const requireFeature = (enabled, names) => {
|
||||
if (!enabled) return;
|
||||
for (const name of names) {
|
||||
if (!env[name]) context.addIssue({ code: "custom", path: [name], message: `${name} is required when enabled` });
|
||||
}
|
||||
};
|
||||
requireFeature(env.ENABLE_MAIL, ["MAIL_HOST", "MAIL_PORT", "MAIL_USER", "MAIL_PASS", "MAIL_FROM"]);
|
||||
requireFeature(env.ENABLE_S3, ["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_S3_BUCKET_NAME"]);
|
||||
});
|
||||
|
||||
let validatedEnv;
|
||||
const validateEnvironment = (source = process.env) => {
|
||||
const result = envSchema.safeParse(source);
|
||||
if (!result.success) {
|
||||
const names = [...new Set(result.error.issues.map((issue) => issue.path.join(".") || "environment"))];
|
||||
throw new Error(`Invalid environment configuration: ${names.join(", ")}`);
|
||||
}
|
||||
validatedEnv = result.data;
|
||||
return validatedEnv;
|
||||
};
|
||||
const getEnvironment = () => validatedEnv || validateEnvironment();
|
||||
const getOptionalFeatureStatus = (env = process.env) => ({
|
||||
mail: Boolean(env.MAIL_HOST && env.MAIL_PORT && env.MAIL_USER && env.MAIL_PASS && env.MAIL_FROM),
|
||||
s3: Boolean(env.AWS_REGION && env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY && env.AWS_S3_BUCKET_NAME),
|
||||
docs: Boolean(env.DOCS_USER && env.DOCS_PASS),
|
||||
});
|
||||
|
||||
module.exports = { validateEnvironment, getEnvironment, getOptionalFeatureStatus };
|
||||
Reference in New Issue
Block a user