143 lines
7.1 KiB
TypeScript
143 lines
7.1 KiB
TypeScript
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'),
|
|
// --- Base de datos de SAP B1 (consultas de solo lectura registradas): opcional; sin motor el módulo queda sin configurar ---
|
|
SAPDB_ENGINE: z.preprocess(emptyToUndefined, z.enum(['hana']).optional()),
|
|
SAPDB_HOST: z.preprocess(emptyToUndefined, z.string().optional()),
|
|
SAPDB_PORT: z.coerce.number().int().min(1).max(65535).default(30015),
|
|
SAPDB_USER: z.preprocess(emptyToUndefined, z.string().optional()),
|
|
SAPDB_PASSWORD: z.preprocess(emptyToUndefined, z.string().optional()),
|
|
// Esquema de la empresa (en HANA, el nombre de la base: `SBO_TAGUMEDICA_21`).
|
|
SAPDB_SCHEMA: z.preprocess(emptyToUndefined, z.string().regex(/^\w+$/, 'solo letras, dígitos y «_»').optional()),
|
|
SAPDB_TLS: z
|
|
.enum(['true', 'false'])
|
|
.default('false')
|
|
.transform((v) => v === 'true'),
|
|
SAPDB_CA_FILE: z.preprocess(emptyToUndefined, z.string().optional()),
|
|
SAPDB_VERIFY_SSL: z
|
|
.enum(['true', 'false'])
|
|
.default('true')
|
|
.transform((v) => v === 'true'),
|
|
// Espera máxima (segundos) de cada consulta.
|
|
SAPDB_TIMEOUT: z.coerce.number().positive().default(30),
|
|
// Consultas que se ejecutan a la vez; el resto espera su turno (la base de SAP no es para esto).
|
|
SAPDB_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
|
|
// Tope de filas de cualquier consulta; lo que pase se recorta.
|
|
SAPDB_MAX_ROWS: z.coerce.number().int().positive().default(5000),
|
|
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.SAPDB_CA_FILE && !existsSync(resolve(env.SAPDB_CA_FILE))) {
|
|
ctx.addIssue({ code: 'custom', path: ['SAPDB_CA_FILE'], message: `no existe el archivo ${resolve(env.SAPDB_CA_FILE)}` });
|
|
}
|
|
if (env.SAPDB_ENGINE && !(env.SAPDB_HOST && env.SAPDB_USER && env.SAPDB_PASSWORD && env.SAPDB_SCHEMA)) {
|
|
ctx.addIssue({ code: 'custom', path: ['SAPDB_ENGINE'], message: 'con SAPDB_ENGINE hacen falta SAPDB_HOST, SAPDB_USER, SAPDB_PASSWORD y SAPDB_SCHEMA' });
|
|
}
|
|
if (env.NODE_ENV === 'production' && env.SAPDB_TLS && !env.SAPDB_VERIFY_SSL) {
|
|
ctx.addIssue({ code: 'custom', path: ['SAPDB_VERIFY_SSL'], message: 'no se permite desactivar la validación SSL en producción; use SAPDB_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;
|
|
}
|