Initial upload with CI/CD workflow
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-31 20:27:36 +00:00
parent d44283c1d2
commit ea09db6ba4

190
src/core/validator.ts Normal file
View File

@@ -0,0 +1,190 @@
import { cleanEnv, str, num, bool, email, url, port, json, host, makeValidator } from 'envalid';
import { EnvSchema, EnvType, ValidationResult, ValidationError } from './types';
const enumValidator = makeValidator((input: string) => {
return input;
});
export function createValidator(schema: EnvSchema) {
const validators: Record<string, ReturnType<typeof str | typeof num | typeof bool | typeof email | typeof url | typeof port | typeof json | typeof makeValidator>> = {};
for (const [key, config] of Object.entries(schema.variables)) {
switch (config.type) {
case 'string':
validators[key] = str({ default: config.default as string | undefined, desc: config.desc });
break;
case 'number':
case 'int':
validators[key] = num({ default: config.default as number | undefined, desc: config.desc });
break;
case 'boolean':
validators[key] = bool({ default: config.default as boolean | undefined, desc: config.desc });
break;
case 'email':
validators[key] = email({ default: config.default as string | undefined, desc: config.desc });
break;
case 'url':
validators[key] = url({ default: config.default as string | undefined, desc: config.desc });
break;
case 'port':
validators[key] = port({ default: config.default as number | undefined, desc: config.desc });
break;
case 'json':
validators[key] = json({ default: config.default as string | undefined, desc: config.desc });
break;
case 'enum':
validators[key] = enumValidator({
choices: config.choices || [],
default: config.default as string | undefined,
desc: config.desc
});
break;
case 'host':
validators[key] = host({ default: config.default as string | undefined, desc: config.desc });
break;
default:
validators[key] = str({ default: config.default as string | undefined, desc: config.desc });
}
}
return cleanEnv(process.env, validators);
}
export function validateEnv(envData: Record<string, string>, schema: EnvSchema): ValidationResult {
const errors: ValidationError[] = [];
for (const [key, config] of Object.entries(schema.variables)) {
const value = envData[key];
if (config.required !== false && (value === undefined || value === '')) {
errors.push({
field: key,
message: `Required variable "${key}" is missing`,
value
});
continue;
}
if (value !== undefined && value !== '') {
try {
validateType(key, value, config.type, config.choices);
} catch (e) {
errors.push({
field: key,
message: (e as Error).message,
value
});
}
}
}
return {
valid: errors.length === 0,
errors,
output: {}
};
}
function validateType(key: string, value: string, type: EnvType, choices?: string[]): void {
switch (type) {
case 'number':
case 'int':
if (isNaN(Number(value))) {
throw new Error(`"${key}" must be a valid number, got: ${value}`);
}
break;
case 'boolean':
if (value !== 'true' && value !== 'false' && value !== '1' && value !== '0') {
throw new Error(`"${key}" must be a boolean (true/false), got: ${value}`);
}
break;
case 'port': {
const port = Number(value);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error(`"${key}" must be a valid port number (1-65535), got: ${value}`);
}
break;
}
case 'email': {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new Error(`"${key}" must be a valid email, got: ${value}`);
}
break;
}
case 'url':
try {
new URL(value);
} catch {
throw new Error(`"${key}" must be a valid URL, got: ${value}`);
}
break;
case 'json':
try {
JSON.parse(value);
} catch {
throw new Error(`"${key}" must be valid JSON, got: ${value}`);
}
break;
case 'enum':
if (choices && !choices.includes(value)) {
throw new Error(`"${key}" must be one of: ${choices.join(', ')}, got: ${value}`);
}
break;
}
}
export function parseSchemaFromEnv(envData: Record<string, string>): EnvSchema {
const variables: Record<string, { type: EnvType; required: boolean; desc?: string; default?: string | number | boolean }> = {};
for (const [key, value] of Object.entries(envData)) {
const inferred = inferType(key, value);
variables[key] = {
type: inferred.type,
required: true,
desc: `Environment variable ${key}`
};
}
return { variables };
}
function inferType(key: string, value: string): { type: EnvType; default?: string | number | boolean } {
if (value === '') {
return { type: 'string', default: '' };
}
if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false' || value === '1' || value === '0') {
return { type: 'boolean' };
}
if (!isNaN(Number(value)) && value.trim() !== '') {
return { type: Number.isInteger(Number(value)) ? 'int' : 'number' };
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailRegex.test(value)) {
return { type: 'email' };
}
try {
new URL(value);
return { type: 'url' };
} catch {
// Not a URL
}
const port = Number(value);
if (!isNaN(port) && port >= 1 && port <= 65535) {
return { type: 'port' };
}
try {
JSON.parse(value);
return { type: 'json' };
} catch {
// Not JSON
}
return { type: 'string' };
}