Files
basic-erp/apps/api/src/config/env.schema.ts
T

110 lines
5.0 KiB
TypeScript
Raw Normal View History

import { existsSync } from 'node:fs';
import { resolve } from 'node:path';
import { z } from 'zod';
const emptyToUndefined = (v: unknown) => (v === '' ? undefined : v);
export const envSchema = z
.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().int().positive().default(3000),
// Orígenes permitidos por CORS, separados por coma. Vacío = mismo origen.
CORS_ORIGINS: z
.string()
.default('')
.transform((v) =>
v
.split(',')
.map((s) => s.trim())
.filter(Boolean),
),
LOG_LEVEL: z.enum(['error', 'warn', 'log', 'debug', 'verbose']).default('log'),
// Base SQLite local (permisos de menú). `:memory:` para pruebas.
SQLITE_PATH: z.string().min(1).default('./data/app.sqlite'),
// Usuarios web (U_WEB_USER) que siempre tienen acceso total ("*"); se
// garantiza al arrancar para no quedar fuera por un error de permisos.
ADMIN_WEB_USERS: z
.string()
.default('')
.transform((v) =>
v
.split(',')
.map((s) => s.trim())
.filter(Boolean),
),
// Inactividad tras la cual caduca la sesión web (deslizante).
SESSION_TIMEOUT_MINUTES: z.coerce.number().int().positive().default(30),
// --- SAP Business One Service Layer ---
SAPB1_SL_URL: z
.url()
.transform((v) => v.replace(/\/+$/, '')),
SAPB1_SL_COMPANYDB: z.string().min(1),
SAPB1_SL_USER: z.string().min(1),
SAPB1_SL_PASSWORD: z.string().min(1),
SAPB1_SL_TIMEOUT_MS: z.coerce.number().int().positive().default(15000),
// Peticiones simultáneas permitidas hacia el Service Layer; el resto se encola.
// Medido en SAP10 (12 peticiones): con 1 -> 0.6 s en total; con 2 ya tarda 7.5 s y con
// 4, 22 s (el Service Layer retrasa ~7 s por cada petición paralela extra). Default: 1.
SAPB1_SL_MAX_CONCURRENCY: z.coerce.number().int().positive().default(1),
// CA adicional de confianza (PEM): certificado autofirmado del Service
// Layer o CA de un firewall que inspecciona SSL. Opcional.
SAPB1_SL_CA_FILE: z.preprocess(emptyToUndefined, z.string().optional()),
// --- SAP Business One API Gateway (reportes Crystal): opcional; sin URL el módulo queda sin configurar ---
APIGATEWAY_URL: z.preprocess(emptyToUndefined, z.url().optional()).transform((v) => v?.replace(/\/+$/, '')),
APIGATEWAY_USER: z.preprocess(emptyToUndefined, z.string().optional()),
APIGATEWAY_PASSWORD: z.preprocess(emptyToUndefined, z.string().optional()),
APIGATEWAY_COMPANYDB: z.preprocess(emptyToUndefined, z.string().optional()),
APIGATEWAY_DBINSTANCE: z.preprocess(emptyToUndefined, z.string().optional()),
// Espera máxima (segundos) de cada llamada: generar un reporte puede tardar.
APIGATEWAY_TIMEOUT: z.coerce.number().positive().default(60),
// Reportes que se generan a la vez; el resto espera su turno.
APIGATEWAY_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
APIGATEWAY_CA_FILE: z.preprocess(emptyToUndefined, z.string().optional()),
APIGATEWAY_VERIFY_SSL: z
.enum(['true', 'false'])
.default('true')
.transform((v) => v === 'true'),
SAPB1_SL_VERIFY_SSL: z
.enum(['true', 'false'])
.default('true')
.transform((v) => v === 'true'),
})
.superRefine((env, ctx) => {
if (env.SAPB1_SL_CA_FILE && !existsSync(resolve(env.SAPB1_SL_CA_FILE))) {
ctx.addIssue({
code: 'custom',
path: ['SAPB1_SL_CA_FILE'],
message: `no existe el archivo ${resolve(env.SAPB1_SL_CA_FILE)}`,
});
}
if (env.APIGATEWAY_CA_FILE && !existsSync(resolve(env.APIGATEWAY_CA_FILE))) {
ctx.addIssue({ code: 'custom', path: ['APIGATEWAY_CA_FILE'], message: `no existe el archivo ${resolve(env.APIGATEWAY_CA_FILE)}` });
}
if (env.APIGATEWAY_URL && !(env.APIGATEWAY_USER && env.APIGATEWAY_PASSWORD && env.APIGATEWAY_COMPANYDB)) {
ctx.addIssue({ code: 'custom', path: ['APIGATEWAY_URL'], message: 'con APIGATEWAY_URL hacen falta APIGATEWAY_USER, APIGATEWAY_PASSWORD y APIGATEWAY_COMPANYDB' });
}
if (env.NODE_ENV === 'production' && !env.APIGATEWAY_VERIFY_SSL) {
ctx.addIssue({ code: 'custom', path: ['APIGATEWAY_VERIFY_SSL'], message: 'no se permite desactivar la validación SSL en producción; use APIGATEWAY_CA_FILE' });
}
if (env.NODE_ENV === 'production' && !env.SAPB1_SL_VERIFY_SSL) {
ctx.addIssue({
code: 'custom',
path: ['SAPB1_SL_VERIFY_SSL'],
message: 'no se permite desactivar la validación SSL en producción; use SAPB1_SL_CA_FILE',
});
}
});
export type Env = z.infer<typeof envSchema>;
export function validateEnv(raw: Record<string, unknown>): Env {
const result = envSchema.safeParse(raw);
if (!result.success) {
const detail = result.error.issues
.map((i) => `${i.path.join('.')}: ${i.message}`)
.join('; ');
throw new Error(`Configuración inválida (.env): ${detail}`);
}
return result.data;
}