This commit is contained in:
190
src/core/validator.ts
Normal file
190
src/core/validator.ts
Normal 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' };
|
||||
}
|
||||
Reference in New Issue
Block a user