v0.4.7
This commit is contained in:
parent
3a1c7e0844
commit
71b98fb6c2
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "WEB: Vite (Chrome)",
|
||||
|
||||
29
Dockerfile
29
Dockerfile
@ -44,6 +44,35 @@ RUN mkdir -p /usr/share/fonts/truetype/barlow \
|
||||
&& fc-cache -f -v
|
||||
|
||||
|
||||
## Configurar locale, timezone y variables de entorno para España
|
||||
|
||||
# Evita prompts interactivos
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Instalar paquetes necesarios
|
||||
RUN apt-get update && apt-get install -y \
|
||||
locales \
|
||||
tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Generar locale español de España UTF-8
|
||||
RUN sed -i '/es_ES.UTF-8/s/^# //g' /etc/locale.gen \
|
||||
&& locale-gen
|
||||
|
||||
# Configurar variables de entorno de locale
|
||||
ENV LANG=es_ES.UTF-8 \
|
||||
LANGUAGE=es_ES:es \
|
||||
LC_ALL=es_ES.UTF-8
|
||||
|
||||
# Configurar zona horaria España
|
||||
ENV TZ=Europe/Madrid
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
|
||||
&& echo $TZ > /etc/timezone
|
||||
|
||||
# Verificación opcional
|
||||
# RUN locale && date
|
||||
|
||||
|
||||
# Reducir tamaño de la imagen
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { Account, AccountStatus } from "@/contexts/accounts/domain/";
|
||||
import {
|
||||
EmailAddress,
|
||||
@ -11,8 +13,8 @@ import {
|
||||
type MapperParamsType,
|
||||
SequelizeMapper,
|
||||
} from "@/core/common/infrastructure/sequelize/sequelize-mapper";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { AccountCreationAttributes, AccountModel } from "../sequelize/account.model";
|
||||
|
||||
import type { AccountCreationAttributes, AccountModel } from "../sequelize/account.model";
|
||||
|
||||
export interface IAccountMapper
|
||||
extends ISequelizeMapper<AccountModel, AccountCreationAttributes, Account> {}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/factuges-server",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --config tsup.config.ts",
|
||||
@ -38,6 +38,7 @@
|
||||
"@erp/customer-invoices": "workspace:*",
|
||||
"@erp/customers": "workspace:*",
|
||||
"@repo/rdx-logger": "workspace:*",
|
||||
"@repo/rdx-utils": "workspace:*",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cls-rtracer": "^2.6.3",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@ -3,12 +3,14 @@ import express, { type Application } from "express";
|
||||
import helmet from "helmet";
|
||||
import responseTime from "response-time";
|
||||
|
||||
import type { ConfigType } from "./config";
|
||||
|
||||
// ❗️ No cargamos dotenv aquí. Debe hacerse en el entrypoint o en ./config.
|
||||
// dotenv.config();
|
||||
import { ENV } from "./config";
|
||||
|
||||
import { logger } from "./lib/logger";
|
||||
|
||||
export function createApp(): Application {
|
||||
export function createApp(config: ConfigType): Application {
|
||||
const app = express();
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
@ -30,7 +32,7 @@ export function createApp(): Application {
|
||||
};
|
||||
|
||||
const prodCors: CorsOptions = {
|
||||
origin: ENV.FRONTEND_URL,
|
||||
origin: config.server.frontendUrl,
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
exposedHeaders: [
|
||||
@ -44,10 +46,10 @@ export function createApp(): Application {
|
||||
],
|
||||
};
|
||||
|
||||
app.use(cors(ENV.NODE_ENV === "development" ? devCors : prodCors));
|
||||
app.options("*", cors(ENV.NODE_ENV === "development" ? devCors : prodCors));
|
||||
app.use(cors(config.flags.isDev ? devCors : prodCors));
|
||||
app.options("*", cors(config.flags.isDev ? devCors : prodCors));
|
||||
|
||||
app.set("port", process.env.API_PORT ?? 3002);
|
||||
app.set("port", config.server.port ?? 3002);
|
||||
|
||||
// Oculta la cabecera x-powered-by
|
||||
app.disable("x-powered-by");
|
||||
|
||||
12
apps/server/src/config/company-certificates.json
Normal file
12
apps/server/src/config/company-certificates.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"acme": {
|
||||
"certificateId": "acme-prod-cert-v1",
|
||||
"certificateSecretName": "acme_cert",
|
||||
"certificatePasswordSecretName": "acme_cert_pwd"
|
||||
},
|
||||
"rodax": {
|
||||
"certificateId": "no se que poner aqui",
|
||||
"certificateSecretName": "certificate_secret_name",
|
||||
"certificatePasswordSecretName": "certificate_password_secret_name"
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
export function asNumber(value: string | undefined, fallback: number): number {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
export function asBoolean(value: string | undefined, fallback: boolean): boolean {
|
||||
if (value === undefined) return fallback;
|
||||
return ["1", "true", "yes", "on"].includes(String(value).toLowerCase());
|
||||
}
|
||||
export function required(name: string, value: string | undefined): string {
|
||||
if (!value) throw new Error(`Missing required environment variable: ${name}`);
|
||||
return value;
|
||||
}
|
||||
154
apps/server/src/config/config.ts
Normal file
154
apps/server/src/config/config.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import type { BaseConfigType } from "@erp/core/api";
|
||||
import type { ILogger } from "@repo/rdx-logger";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import { formatZodErrors } from "./env-error";
|
||||
import { EnvSchema } from "./env-schema";
|
||||
import { loadCompanyCertificates } from "./load-company-certificates";
|
||||
import { logConfigSummary } from "./log-config";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
let _config: ConfigType | null = null;
|
||||
|
||||
export type ConfigType = BaseConfigType & {
|
||||
env: string;
|
||||
|
||||
server: {
|
||||
port: number;
|
||||
apiBasePath: string;
|
||||
frontendUrl: string;
|
||||
trustProxy: number;
|
||||
timezone: string;
|
||||
};
|
||||
|
||||
database: {
|
||||
dialect: "mysql" | "postgres" | "sqlite" | "mariadb" | "mssql" | "db2" | "snowflake" | "oracle";
|
||||
host: string;
|
||||
port: number;
|
||||
name: string;
|
||||
user: string;
|
||||
password: string;
|
||||
logging: boolean;
|
||||
ssl: boolean;
|
||||
syncMode: string;
|
||||
};
|
||||
|
||||
auth: {
|
||||
jwt: {
|
||||
secret: string;
|
||||
accessExpiration: string;
|
||||
refreshExpiration: string;
|
||||
};
|
||||
};
|
||||
|
||||
paths: {
|
||||
templates: string;
|
||||
documents: string;
|
||||
signedDocuments: string;
|
||||
fastReportBin: string;
|
||||
};
|
||||
|
||||
signingService: {
|
||||
url: string;
|
||||
method: string;
|
||||
timeoutMs: number;
|
||||
maxRetries: number;
|
||||
};
|
||||
|
||||
certificates?: Record<string, unknown>;
|
||||
|
||||
flags: {
|
||||
isProd: boolean;
|
||||
isDev: boolean;
|
||||
isTest: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const initConfig = (logger: ILogger): ConfigType => {
|
||||
if (_config) {
|
||||
return _config;
|
||||
}
|
||||
|
||||
const parsed = EnvSchema.safeParse(process.env);
|
||||
|
||||
if (!parsed.success) {
|
||||
const errors = formatZodErrors(parsed.error);
|
||||
|
||||
logger.error("Invalid environment configuration", {
|
||||
errorCount: errors.length,
|
||||
errors,
|
||||
});
|
||||
|
||||
throw new Error("Invalid environment configuration");
|
||||
}
|
||||
|
||||
const env = parsed.data;
|
||||
|
||||
const certificates = loadCompanyCertificates(env.COMPANY_CERTIFICATES_PATH, logger);
|
||||
|
||||
_config = {
|
||||
env: env.NODE_ENV,
|
||||
|
||||
server: {
|
||||
port: env.PORT,
|
||||
apiBasePath: env.API_BASE_PATH,
|
||||
frontendUrl: env.FRONTEND_URL,
|
||||
trustProxy: env.TRUST_PROXY,
|
||||
timezone: env.TIMEZONE,
|
||||
},
|
||||
|
||||
database: {
|
||||
dialect: env.DB_DIALECT,
|
||||
host: env.DB_HOST,
|
||||
port: env.DB_PORT,
|
||||
name: env.DB_NAME,
|
||||
user: env.DB_USER,
|
||||
password: env.DB_PASS,
|
||||
logging: env.DB_LOGGING,
|
||||
ssl: env.DB_SSL,
|
||||
syncMode: env.DB_SYNC_MODE,
|
||||
},
|
||||
|
||||
auth: {
|
||||
jwt: {
|
||||
secret: env.JWT_SECRET,
|
||||
accessExpiration: env.JWT_ACCESS_EXPIRATION,
|
||||
refreshExpiration: env.JWT_REFRESH_EXPIRATION,
|
||||
},
|
||||
},
|
||||
|
||||
paths: {
|
||||
templates: env.TEMPLATES_PATH,
|
||||
documents: env.DOCUMENTS_PATH,
|
||||
signedDocuments: env.SIGNED_DOCUMENTS_PATH,
|
||||
fastReportBin: env.FASTREPORT_BIN,
|
||||
},
|
||||
|
||||
signingService: {
|
||||
url: env.SIGNING_SERVICE_URL,
|
||||
method: env.SIGNING_SERVICE_METHOD,
|
||||
timeoutMs: env.SIGNING_SERVICE_TIMEOUT_MS,
|
||||
maxRetries: env.SIGNING_SERVICE_MAX_RETRIES,
|
||||
},
|
||||
|
||||
certificates,
|
||||
|
||||
flags: {
|
||||
isProd: env.NODE_ENV === "production",
|
||||
isDev: env.NODE_ENV === "development",
|
||||
isTest: env.NODE_ENV === "test",
|
||||
},
|
||||
};
|
||||
|
||||
logConfigSummary(_config, logger);
|
||||
|
||||
return _config;
|
||||
};
|
||||
|
||||
export const Config = (): ConfigType => {
|
||||
if (!_config) {
|
||||
throw new Error("Config not initialized. Call initConfig(logger) during bootstrap.");
|
||||
}
|
||||
return _config;
|
||||
};
|
||||
@ -2,14 +2,14 @@ import { Sequelize } from "sequelize";
|
||||
|
||||
import { logger } from "../lib/logger";
|
||||
|
||||
import { ENV } from "./index";
|
||||
import type { ConfigType } from "./index";
|
||||
|
||||
/**
|
||||
* Crea la instancia de Sequelize según ENV (DATABASE_URL o parámetros sueltos),
|
||||
* autentica la conexión y devuelve la instancia lista para usar.
|
||||
*/
|
||||
export async function tryConnectToDatabase(): Promise<Sequelize> {
|
||||
const sequelize = buildSequelize();
|
||||
export async function tryConnectToDatabase(config: ConfigType): Promise<Sequelize> {
|
||||
const sequelize = buildSequelize(config);
|
||||
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
@ -49,12 +49,12 @@ export async function closeDatabase(sequelize: Sequelize): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function buildSequelize(): Sequelize {
|
||||
function buildSequelize(config: ConfigType): Sequelize {
|
||||
const common = {
|
||||
logging: ENV.DB_LOGGING
|
||||
logging: config.database.logging
|
||||
? (msg: any) => logger.debug(String(msg), { label: "sequelize" })
|
||||
: false,
|
||||
timezone: ENV.APP_TIMEZONE,
|
||||
timezone: config.server.timezone,
|
||||
pool: {
|
||||
max: 10,
|
||||
min: 0,
|
||||
@ -62,27 +62,28 @@ function buildSequelize(): Sequelize {
|
||||
acquire: 30000,
|
||||
},
|
||||
dialectOptions: {
|
||||
timezone: ENV.APP_TIMEZONE,
|
||||
timezone: config.server.timezone,
|
||||
},
|
||||
} as const;
|
||||
|
||||
if (ENV.DATABASE_URL && ENV.DATABASE_URL.trim() !== "") {
|
||||
/*if (ENV.DATABASE_URL && ENV.DATABASE_URL.trim() !== "") {
|
||||
// URL completa (recomendada p.ej. en Postgres/MariaDB)
|
||||
return new Sequelize(ENV.DATABASE_URL, common);
|
||||
}
|
||||
}*/
|
||||
|
||||
// Parámetros sueltos (asegurar requeridos mínimos)
|
||||
if (!ENV.DB_DIALECT) {
|
||||
if (!config.database.dialect) {
|
||||
throw new Error("DB_DIALECT is required when DATABASE_URL is not provided");
|
||||
}
|
||||
if (!(ENV.DB_NAME && ENV.DB_USER)) {
|
||||
|
||||
if (!(config.database.name && config.database.user)) {
|
||||
throw new Error("DB_NAME and DB_USER are required when DATABASE_URL is not provided");
|
||||
}
|
||||
|
||||
return new Sequelize(ENV.DB_NAME, ENV.DB_USER, ENV.DB_PASS, {
|
||||
host: ENV.DB_HOST,
|
||||
port: ENV.DB_PORT,
|
||||
dialect: ENV.DB_DIALECT,
|
||||
return new Sequelize(config.database.name, config.database.user, config.database.password, {
|
||||
host: config.database.host,
|
||||
port: config.database.port,
|
||||
dialect: config.database.dialect,
|
||||
...common,
|
||||
});
|
||||
}
|
||||
|
||||
12
apps/server/src/config/env-error.ts
Normal file
12
apps/server/src/config/env-error.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { ZodError, ZodIssue } from "zod";
|
||||
|
||||
export type EnvValidationError = {
|
||||
path: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const formatZodErrors = (error: ZodError): EnvValidationError[] =>
|
||||
error.issues.map((issue: ZodIssue) => ({
|
||||
path: issue.path.join("."),
|
||||
message: issue.message,
|
||||
}));
|
||||
57
apps/server/src/config/env-schema.ts
Normal file
57
apps/server/src/config/env-schema.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const NodeEnvSchema = z.enum(["development", "test", "production"]);
|
||||
|
||||
const DbSyncModeSchema = z.enum(["none", "alter", "force"]);
|
||||
const DbDialectSchema = z.enum(["postgres", "mysql", "mariadb", "mssql", "sqlite"]);
|
||||
|
||||
export const CompanyCertificateSchema = z.object({
|
||||
certificateId: z.string().min(1),
|
||||
certificateSecretName: z.string().min(1),
|
||||
certificatePasswordSecretName: z.string().min(1),
|
||||
});
|
||||
|
||||
export const CompanyCertificatesSchema = z.record(
|
||||
z.string().regex(/^[a-z0-9-]+$/),
|
||||
CompanyCertificateSchema
|
||||
);
|
||||
|
||||
export const EnvSchema = z.object({
|
||||
NODE_ENV: NodeEnvSchema.default("development"),
|
||||
|
||||
PORT: z.coerce.number().int().positive().default(3002),
|
||||
API_BASE_PATH: z.string(),
|
||||
FRONTEND_URL: z.url(),
|
||||
TIMEZONE: z.string().default("Europe/Madrid"),
|
||||
TRUST_PROXY: z.coerce.number().int().default(0),
|
||||
|
||||
DB_DIALECT: DbDialectSchema.default("mysql"),
|
||||
DB_HOST: z.string().min(1),
|
||||
DB_PORT: z.coerce.number().int().positive().default(3306),
|
||||
DB_NAME: z.string().min(1),
|
||||
DB_USER: z.string().min(1),
|
||||
DB_PASS: z.string().min(1),
|
||||
DB_LOGGING: z.coerce.boolean().default(false),
|
||||
DB_SSL: z.coerce.boolean().default(false),
|
||||
DB_SYNC_MODE: DbSyncModeSchema.default("none"),
|
||||
|
||||
WARMUP_TIMEOUT_MS: z.coerce.number().int().positive(),
|
||||
WARMUP_STRICT: z.coerce.boolean().default(false),
|
||||
|
||||
JWT_SECRET: z.string().min(32),
|
||||
JWT_ACCESS_EXPIRATION: z.string(),
|
||||
JWT_REFRESH_EXPIRATION: z.string(),
|
||||
|
||||
TEMPLATES_PATH: z.string(),
|
||||
DOCUMENTS_PATH: z.string(),
|
||||
SIGNED_DOCUMENTS_PATH: z.string(),
|
||||
|
||||
FASTREPORT_BIN: z.string(),
|
||||
|
||||
SIGNING_SERVICE_URL: z.url(),
|
||||
SIGNING_SERVICE_METHOD: z.enum(["GET", "POST", "PUT"]),
|
||||
SIGNING_SERVICE_TIMEOUT_MS: z.coerce.number().int().positive(),
|
||||
SIGNING_SERVICE_MAX_RETRIES: z.coerce.number().int().nonnegative(),
|
||||
|
||||
COMPANY_CERTIFICATES_PATH: z.string().min(1),
|
||||
});
|
||||
@ -1,96 +1 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import { asBoolean, asNumber, required } from "./config-helpers";
|
||||
|
||||
// Carga de variables de entorno (.env). Si ya están en el entorno, no se sobreescriben.
|
||||
dotenv.config();
|
||||
|
||||
type NodeEnv = "development" | "test" | "production";
|
||||
type DbSyncMode = "none" | "alter" | "force";
|
||||
type DbDialect = "postgres" | "mysql" | "mariadb" | "mssql" | "sqlite";
|
||||
|
||||
const NODE_ENV = (process.env.NODE_ENV as NodeEnv) ?? "development";
|
||||
const isProd = NODE_ENV === "production";
|
||||
const isDev = NODE_ENV === "development";
|
||||
|
||||
const API_PORT = asNumber(process.env.API_PORT, 3002);
|
||||
|
||||
// En producción exigimos FRONTEND_URL definido (según requisitos actuales).
|
||||
const FRONTEND_URL = isProd
|
||||
? required("FRONTEND_URL", process.env.FRONTEND_URL)
|
||||
: (process.env.FRONTEND_URL ?? "http://localhost:5173");
|
||||
|
||||
// Base de datos (dos modos: URL o parámetros sueltos)
|
||||
const DATABASE_URL = process.env.DATABASE_URL; // p.ej. postgres://user:pass@host:5432/dbname
|
||||
|
||||
const DB_DIALECT = (process.env.DB_DIALECT as DbDialect | undefined) ?? "mysql";
|
||||
const DB_HOST = process.env.DB_HOST ?? "localhost";
|
||||
const DB_PORT = asNumber(process.env.DB_PORT, 3306);
|
||||
const DB_NAME = process.env.DB_NAME ?? "";
|
||||
const DB_USER = process.env.DB_USER ?? "";
|
||||
const DB_PASS = process.env.DB_PASS ?? "";
|
||||
|
||||
const DB_LOGGING = asBoolean(process.env.DB_LOGGING, false);
|
||||
|
||||
// Modo de sincronización usado por model-loader.ts
|
||||
const DB_SYNC_MODE =
|
||||
(process.env.DB_SYNC_MODE as DbSyncMode | undefined) ?? (isProd ? "none" : "alter");
|
||||
|
||||
// Opcional: timezone para Sequelize (según necesidades)
|
||||
const APP_TIMEZONE = process.env.APP_TIMEZONE ?? "Europe/Madrid";
|
||||
|
||||
// FASTREPORT_BIN: ruta al ejecutable de FastReport CLI
|
||||
// Si no se define, se buscará en rutas estándar según SO.
|
||||
const FASTREPORT_BIN = process.env.FASTREPORT_BIN;
|
||||
|
||||
// Ruta raíz para plantillas (templates)
|
||||
const TEMPLATES_PATH = process.env.TEMPLATES_PATH ?? "./templates";
|
||||
|
||||
// Documentos
|
||||
const DOCUMENTS_PATH = process.env.DOCUMENTS_PATH ?? "./documents";
|
||||
|
||||
// Proxy (no usáis ahora, pero dejamos la variable por si se activa en el futuro)
|
||||
const TRUST_PROXY = asNumber(process.env.TRUST_PROXY, 0);
|
||||
|
||||
const SIGNING_SERVICE_URL = process.env.SIGNING_SERVICE_URL;
|
||||
const SIGNING_SERVICE_METHOD = process.env.SIGNING_SERVICE_METHOD ?? "POST";
|
||||
const SIGNING_SERVICE_TIMEOUT_MS = asNumber(process.env.SIGNING_SERVICE_TIMEOUT_MS, 15_000);
|
||||
const SIGNING_SERVICE_MAX_RETRIES = asNumber(process.env.SIGNING_SERVICE_MAX_RETRIES, 2);
|
||||
|
||||
const COMPANY_SLUG = process.env.COMPANY_SLUG ?? "acme";
|
||||
const COMPANY_CERTIFICATES_JSON = process.env.COMPANY_CERTIFICATES_JSON ?? {};
|
||||
|
||||
export const ENV = {
|
||||
NODE_ENV,
|
||||
API_PORT,
|
||||
FRONTEND_URL,
|
||||
DATABASE_URL,
|
||||
DB_DIALECT,
|
||||
DB_HOST,
|
||||
DB_PORT,
|
||||
DB_NAME,
|
||||
DB_USER,
|
||||
DB_PASS,
|
||||
DB_LOGGING,
|
||||
DB_SYNC_MODE,
|
||||
APP_TIMEZONE,
|
||||
TRUST_PROXY,
|
||||
|
||||
TEMPLATES_PATH,
|
||||
DOCUMENTS_PATH,
|
||||
FASTREPORT_BIN,
|
||||
|
||||
SIGNING_SERVICE_URL,
|
||||
SIGNING_SERVICE_METHOD,
|
||||
SIGNING_SERVICE_TIMEOUT_MS,
|
||||
SIGNING_SERVICE_MAX_RETRIES,
|
||||
|
||||
COMPANY_SLUG,
|
||||
COMPANY_CERTIFICATES_JSON,
|
||||
} as const;
|
||||
|
||||
export const Flags = {
|
||||
isProd,
|
||||
isDev,
|
||||
isTest: NODE_ENV === "test",
|
||||
} as const;
|
||||
export * from "./config";
|
||||
|
||||
48
apps/server/src/config/load-company-certificates.ts
Normal file
48
apps/server/src/config/load-company-certificates.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import type { ILogger } from "@repo/rdx-logger";
|
||||
|
||||
import { CompanyCertificatesSchema } from "./env-schema";
|
||||
|
||||
export const loadCompanyCertificates = (filePath: string, logger: ILogger) => {
|
||||
try {
|
||||
const absolutePath = path.resolve(filePath);
|
||||
logger.info(filePath, path.dirname(filePath));
|
||||
|
||||
listarDirectorio(path.dirname(filePath));
|
||||
|
||||
const raw = fs.readFileSync(absolutePath, "utf-8");
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
return CompanyCertificatesSchema.parse(parsed);
|
||||
} catch (error) {
|
||||
logger.error("Invalid company certificates configuration", {
|
||||
path: filePath,
|
||||
error,
|
||||
});
|
||||
|
||||
throw new Error("Invalid company certificates configuration");
|
||||
}
|
||||
};
|
||||
|
||||
function listarDirectorio(directorio: string): string[] {
|
||||
try {
|
||||
// Verificar que exista
|
||||
const stats = fs.statSync(directorio);
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error(`La ruta proporcionada no es un directorio: ${directorio}`);
|
||||
}
|
||||
|
||||
// Leer contenido
|
||||
const archivos = fs.readdirSync(directorio);
|
||||
|
||||
// Retornar rutas absolutas (opcional)
|
||||
const result = archivos.map((nombre) => path.join(directorio, nombre));
|
||||
console.log(result);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error al listar el directorio: ${error.message}`);
|
||||
}
|
||||
}
|
||||
35
apps/server/src/config/log-config.ts
Normal file
35
apps/server/src/config/log-config.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { ILogger } from "@repo/rdx-logger";
|
||||
|
||||
import type { ConfigType } from "./config";
|
||||
|
||||
const redact = (value: string): string => {
|
||||
if (value.length <= 4) return "***";
|
||||
return `${value.slice(0, 2)}***${value.slice(-2)}`;
|
||||
};
|
||||
|
||||
export const logConfigSummary = (config: ConfigType, logger: ILogger): void => {
|
||||
logger.info("────────────────────────────────────────");
|
||||
logger.info("Server configuration loaded");
|
||||
logger.info("────────────────────────────────────────");
|
||||
|
||||
logger.info(`ENV : ${config.env}`);
|
||||
logger.info(`API PORT : ${config.server.port}`);
|
||||
logger.info(`API API_BASE_PATH : ${config.server.apiBasePath}`);
|
||||
logger.info(`FRONTEND URL : ${config.server.frontendUrl}`);
|
||||
|
||||
logger.info(
|
||||
`DB : ${config.database.dialect} ${config.database.host}:${config.database.port}/${config.database.name}`
|
||||
);
|
||||
logger.info(`DB SYNC MODE : ${config.database.syncMode}`);
|
||||
|
||||
logger.info(`JWT SECRET : ${redact(config.auth.jwt.secret)}`);
|
||||
|
||||
logger.info(`TEMPLATES PATH : ${config.paths.templates}`);
|
||||
logger.info(`DOCUMENTS PATH : ${config.paths.documents}`);
|
||||
|
||||
logger.info(`SIGNING SERVICE : ${config.signingService.url}`);
|
||||
|
||||
logger.info(`CERTIFICATES : ${Object.keys(config.certificates).join(", ")}`);
|
||||
|
||||
logger.info("────────────────────────────────────────");
|
||||
};
|
||||
@ -5,24 +5,24 @@ import { DateTime } from "luxon";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { createApp } from "./app.ts";
|
||||
import { initConfig } from "./config";
|
||||
import { tryConnectToDatabase } from "./config/database.ts";
|
||||
import { ENV } from "./config/index.ts";
|
||||
import { registerHealthRoutes } from "./health.ts";
|
||||
import { listRoutes, logger } from "./lib/index.ts";
|
||||
import { initModules } from "./lib/modules/index.ts";
|
||||
import { registerModules } from "./register-modules.ts";
|
||||
|
||||
const API_BASE_PATH = "/api/v1";
|
||||
|
||||
z.config(z.locales.es());
|
||||
|
||||
const config = initConfig(logger);
|
||||
|
||||
// Guardamos información del estado del servidor
|
||||
export const currentState = {
|
||||
launchedAt: DateTime.now(),
|
||||
appPath: process.cwd(),
|
||||
hosts: "0.0.0.0",
|
||||
port: ENV.API_PORT,
|
||||
environment: ENV.NODE_ENV,
|
||||
port: config.server.port,
|
||||
environment: config.env,
|
||||
connections: {} as Record<string, unknown>,
|
||||
};
|
||||
|
||||
@ -91,11 +91,11 @@ const serverError = (error: NodeJS.ErrnoException) => {
|
||||
logger.error("⛔️ Server wasn't able to start properly.", {
|
||||
label: "serverError0",
|
||||
error,
|
||||
port: ENV.API_PORT,
|
||||
port: config.database.port,
|
||||
});
|
||||
|
||||
if (error.code === "EADDRINUSE") {
|
||||
logger.error(`Port ${ENV.API_PORT} already in use`, { error, label: "serverError1" });
|
||||
logger.error(`Port ${config.database.port} already in use`, { error, label: "serverError1" });
|
||||
} else {
|
||||
logger.error(error.message, { error, label: "serverError2" });
|
||||
}
|
||||
@ -117,7 +117,7 @@ const serverConnection = (conn: any) => {
|
||||
// Cargar paquetes de la aplicación: customers, invoices, routes...
|
||||
registerModules();
|
||||
|
||||
const app = createApp();
|
||||
const app = createApp(config);
|
||||
|
||||
// Crea el servidor HTTP
|
||||
const server = http.createServer(app);
|
||||
@ -208,41 +208,19 @@ process.on("uncaughtException", async (error: Error) => {
|
||||
logger.info(`Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`);
|
||||
logger.info(`Process PID: ${process.pid}`);
|
||||
|
||||
// Mostrar variables de entorno
|
||||
logger.info(`Environment: ${currentState.environment}`);
|
||||
logger.info(`API_PORT: ${ENV.API_PORT}`);
|
||||
logger.info(`API_BASE_PATH: ${API_BASE_PATH}`);
|
||||
|
||||
logger.info(`FRONTEND_URL: ${ENV.FRONTEND_URL}`);
|
||||
|
||||
logger.info(`DB_DIALECT: ${ENV.DB_DIALECT}`);
|
||||
logger.info(`DB_HOST: ${ENV.DB_HOST}`);
|
||||
logger.info(`DB_PORT: ${ENV.DB_PORT}`);
|
||||
logger.info(`DB_NAME: ${ENV.DB_NAME}`);
|
||||
logger.info(`DB_USER: ${ENV.DB_USER}`);
|
||||
logger.info(`DB_LOGGING: ${ENV.DB_LOGGING}`);
|
||||
logger.info(`DB_SYNC_MODE: ${ENV.DB_SYNC_MODE}`);
|
||||
|
||||
logger.info(`FASTREPORT_BIN: ${ENV.FASTREPORT_BIN}`);
|
||||
logger.info(`TEMPLATES_PATH: ${ENV.TEMPLATES_PATH}`);
|
||||
|
||||
logger.info(`SIGNING_SERVICE_URL: ${ENV.SIGNING_SERVICE_URL}`);
|
||||
logger.info(`SIGNING_SERVICE_METHOD: ${ENV.SIGNING_SERVICE_METHOD}`);
|
||||
|
||||
const database = await tryConnectToDatabase();
|
||||
const database = await tryConnectToDatabase(config);
|
||||
|
||||
// Lógica de inicialización de DB, si procede:
|
||||
// initStructure(sequelizeConn.connection);
|
||||
// insertUsers();
|
||||
|
||||
// ➕ Rutas de salud disponibles desde el inicio del proceso
|
||||
registerHealthRoutes(app, API_BASE_PATH, () => ({ ready: isReady }));
|
||||
registerHealthRoutes(app, config.server.apiBasePath, () => ({ ready: isReady }));
|
||||
|
||||
await initModules({
|
||||
env: ENV,
|
||||
config,
|
||||
app,
|
||||
database,
|
||||
baseRoutePath: API_BASE_PATH,
|
||||
logger,
|
||||
});
|
||||
|
||||
@ -284,7 +262,7 @@ process.on("uncaughtException", async (error: Error) => {
|
||||
logger.info(`Server environment: ${currentState.environment}`);
|
||||
logger.info("To shut down your server, press <CTRL> + C at any time");
|
||||
|
||||
logger.debug(JSON.stringify(listRoutes((app as any)._router, API_BASE_PATH), null, 3));
|
||||
logger.debug(JSON.stringify(listRoutes((app as any)._router), null, 3));
|
||||
});
|
||||
} catch (error) {
|
||||
// Arranque fallido → readiness sigue false
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import expressListRoutes from "express-list-routes";
|
||||
|
||||
// Función para listar rutas
|
||||
export function listRoutes(app, basePath = "") {
|
||||
return expressListRoutes(app, { logger: false, prefix: basePath });
|
||||
export function listRoutes(app) {
|
||||
return expressListRoutes(app, { logger: false, prefix: "" });
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { ModuleParams } from "@erp/core/api";
|
||||
|
||||
import { ENV } from "../../config";
|
||||
import { logger } from "../logger";
|
||||
|
||||
/**
|
||||
@ -119,22 +118,23 @@ export const initModels = async (params: ModuleParams) => {
|
||||
}
|
||||
|
||||
// 3) Sincronizar base de datos (según modo)
|
||||
const nodeEnv = ENV.NODE_ENV ?? process.env.NODE_ENV ?? "development";
|
||||
const syncModeEnv = (ENV as any).DB_SYNC_MODE ?? process.env.DB_SYNC_MODE; // "none" | "alter" | "force"
|
||||
const defaultMode = nodeEnv === "production" ? "none" : "alter";
|
||||
const mode = (syncModeEnv ?? defaultMode) as "none" | "alter" | "force";
|
||||
const syncModeEnv = params.database.syncMode; // "none" | "alter" | "force"
|
||||
|
||||
logger.info(`ℹ️ Database sync mode ${syncModeEnv}`, { label: "initModels" });
|
||||
|
||||
try {
|
||||
if (mode === "none") {
|
||||
if (syncModeEnv === "none") {
|
||||
logger.info("✔️ Database sync skipped (mode=none)", { label: "initModels" });
|
||||
} else if (mode === "alter") {
|
||||
} else if (syncModeEnv === "alter") {
|
||||
await database.sync({ force: false, alter: true });
|
||||
logger.info("✔️ Database synchronized successfully (mode=alter).", { label: "initModels" });
|
||||
} else if (mode === "force") {
|
||||
} else if (syncModeEnv === "force") {
|
||||
await database.sync({ force: true, alter: false });
|
||||
logger.warn("⚠️ Database synchronized with FORCE (mode=force).", { label: "initModels" });
|
||||
} else {
|
||||
logger.warn(`⚠️ Unknown DB sync mode "${String(mode)}" → skipping`, { label: "initModels" });
|
||||
logger.warn(`⚠️ Unknown DB sync mode "${String(syncModeEnv)}" → skipping`, {
|
||||
label: "initModels",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
|
||||
Binary file not shown.
@ -1,14 +0,0 @@
|
||||
{
|
||||
"mimeType": "application/pdf",
|
||||
"filename": "issued-invoice.pdf",
|
||||
"metadata": {
|
||||
"documentType": "issued-invoice",
|
||||
"documentId": "019c1ef1-7c3d-79ed-a12f-995cdc40252f",
|
||||
"companyId": "5e4dc5b3-96b9-4968-9490-14bd032fec5f",
|
||||
"companySlug": "rodax",
|
||||
"format": "PDF",
|
||||
"languageCode": "es",
|
||||
"filename": "factura-021.pdf",
|
||||
"cacheKey": "issued-invoice:5e4dc5b3-96b9-4968-9490-14bd032fec5f:021:v1"
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/factuges-web",
|
||||
"private": true,
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host --clearScreen false",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/auth",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/core",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -6,5 +6,5 @@ export interface IDocumentMetadata {
|
||||
readonly format: "PDF" | "HTML";
|
||||
readonly languageCode: string;
|
||||
readonly filename: string;
|
||||
readonly cacheKey?: string; // opcional
|
||||
readonly storageKey?: string; // opcional
|
||||
}
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
import type { IDocumentMetadata } from "../application-models";
|
||||
|
||||
export class DocumentCacheKeyFactory {
|
||||
static fromMetadata(metadata: IDocumentMetadata): string {
|
||||
return;
|
||||
return [
|
||||
metadata.documentType,
|
||||
metadata.companyId,
|
||||
metadata.documentId,
|
||||
metadata.format,
|
||||
metadata.languageCode,
|
||||
metadata.filename,
|
||||
metadata.cacheKey,
|
||||
].join(":");
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import type { IDocument } from "@erp/core/api/application";
|
||||
|
||||
export interface IDocumentCacheStore {
|
||||
/**
|
||||
* Devuelve el documento firmado si existe y es válido.
|
||||
* - null => no existe / no válido
|
||||
* - nunca lanza por errores técnicos (best-effort)
|
||||
*/
|
||||
get(cacheKey: string): Promise<IDocument | null>;
|
||||
|
||||
/**
|
||||
* Guarda un documento firmado en cache.
|
||||
* - best-effort
|
||||
* - errores se ignoran o se loguean
|
||||
*/
|
||||
set(cacheKey: string, document: IDocument): Promise<void>;
|
||||
}
|
||||
@ -48,9 +48,9 @@ export class DocumentGenerationService<TSnapshot> {
|
||||
// 2. Pre-processors (cache / short-circuit)
|
||||
for (const preProcessor of this.preProcessors) {
|
||||
try {
|
||||
const cached = await preProcessor.tryResolve(metadata);
|
||||
if (cached) {
|
||||
return Result.ok(cached);
|
||||
const cachedDoc = await preProcessor.tryResolve(metadata);
|
||||
if (cachedDoc) {
|
||||
return Result.ok(cachedDoc);
|
||||
}
|
||||
} catch (error) {
|
||||
// best-effort: ignorar y continuar
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
export class DocumentStorageKeyFactory {
|
||||
static fromMetadataRecord(metadata: Record<string, unknown>): string {
|
||||
return createHash("sha256").update(JSON.stringify(metadata)).digest("hex");
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,24 @@
|
||||
import type { IDocument } from "../application-models";
|
||||
|
||||
export interface IDocumentStorage {
|
||||
/**
|
||||
* Determina si un existe una clave
|
||||
*
|
||||
* - Side-effect
|
||||
* - Best-effort
|
||||
* - Nunca lanza (errores se gestionan internamente)
|
||||
*/
|
||||
existsKeyStorage(storageKey: string): Promise<Boolean>;
|
||||
|
||||
/**
|
||||
* Recupera un documento guardado.
|
||||
*
|
||||
* - Side-effect
|
||||
* - Best-effort
|
||||
* - Nunca lanza (errores se gestionan internamente)
|
||||
*/
|
||||
readDocument(storageKey: string): Promise<IDocument | null>;
|
||||
|
||||
/**
|
||||
* Persiste un documento generado.
|
||||
*
|
||||
@ -8,5 +26,5 @@ export interface IDocumentStorage {
|
||||
* - Best-effort
|
||||
* - Nunca lanza (errores se gestionan internamente)
|
||||
*/
|
||||
save(document: IDocument, metadata: Record<string, unknown>): Promise<void>;
|
||||
saveDocument(document: IDocument, metadataRecord: Record<string, unknown>): Promise<void>;
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
export * from "./document-cache.interface";
|
||||
export * from "./document-cache-key-factory";
|
||||
export * from "./document-generation-error";
|
||||
export * from "./document-generation-service";
|
||||
export * from "./document-metadata-factory.interface";
|
||||
@ -10,4 +8,5 @@ export * from "./document-renderer.interface";
|
||||
export * from "./document-side-effect.interface";
|
||||
export * from "./document-signing-service.interface";
|
||||
export * from "./document-storage.interface";
|
||||
export * from "./document-storage-key-factory";
|
||||
export * from "./signing-context-resolver.interface";
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
export type BaseConfigType = {
|
||||
env: string;
|
||||
|
||||
flags: {
|
||||
isProd: boolean;
|
||||
isDev: boolean;
|
||||
isTest: boolean;
|
||||
};
|
||||
};
|
||||
1
modules/core/src/api/infrastructure/config/index.ts
Normal file
1
modules/core/src/api/infrastructure/config/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./base-config";
|
||||
@ -1,42 +1,37 @@
|
||||
import type { ModuleParams } from "../../modules";
|
||||
import {
|
||||
EnvCompanySigningContextResolver,
|
||||
FastReportExecutableResolver,
|
||||
FastReportProcessRunner,
|
||||
FastReportRenderer,
|
||||
FastReportTemplateResolver,
|
||||
FilesystemDocumentCacheStore,
|
||||
FilesystemDocumentStorage,
|
||||
RestDocumentSigningService,
|
||||
} from "../documents";
|
||||
import { FilesystemDocumentStorage } from "../storage";
|
||||
|
||||
export const buildCoreDocumentsDI = (env: NodeJS.ProcessEnv) => {
|
||||
const { TEMPLATES_PATH } = env;
|
||||
export const buildCoreDocumentsDI = (params: ModuleParams) => {
|
||||
const {
|
||||
config: { paths, signingService },
|
||||
} = params;
|
||||
|
||||
// Renderers
|
||||
const frExecutableResolver = new FastReportExecutableResolver(env.FASTREPORT_BIN);
|
||||
const frExecutableResolver = new FastReportExecutableResolver(paths.fastReportBin);
|
||||
const frProcessRunner = new FastReportProcessRunner();
|
||||
const fastReportRenderer = new FastReportRenderer(frExecutableResolver, frProcessRunner);
|
||||
const fastReportTemplateResolver = new FastReportTemplateResolver(TEMPLATES_PATH!);
|
||||
const fastReportTemplateResolver = new FastReportTemplateResolver(paths.templates);
|
||||
|
||||
// Signing
|
||||
const signingContextResolver = new EnvCompanySigningContextResolver(env);
|
||||
const signingContextResolver = new EnvCompanySigningContextResolver(params);
|
||||
|
||||
const signingService = new RestDocumentSigningService({
|
||||
signUrl: String(env.SIGNING_SERVICE_URL),
|
||||
method: String(env.SIGNING_SERVICE_METHOD),
|
||||
timeoutMs: env.SIGNING_SERVICE_TIMEOUT_MS
|
||||
? Number.parseInt(env.SIGNING_SERVICE_TIMEOUT_MS, 10)
|
||||
: 15_000,
|
||||
maxRetries: env.SIGNING_SERVICE_MAX_RETRIES
|
||||
? Number.parseInt(env.SIGNING_SERVICE_MAX_RETRIES, 10)
|
||||
: 2,
|
||||
const restSigningService = new RestDocumentSigningService({
|
||||
signUrl: String(signingService.url),
|
||||
method: String(signingService.method),
|
||||
timeoutMs: signingService.timeoutMs,
|
||||
maxRetries: signingService.timeoutMs.maxRetries,
|
||||
});
|
||||
|
||||
// Cache para documentos firmados
|
||||
const cacheStore = new FilesystemDocumentCacheStore(String(env.SIGNED_DOCUMENTS_CACHE_PATH));
|
||||
|
||||
// Almancenamiento para documentos firmados
|
||||
const storage = new FilesystemDocumentStorage(String(env.SIGNED_DOCUMENTS_PATH));
|
||||
const storage = new FilesystemDocumentStorage(String(paths.signedDocuments));
|
||||
|
||||
return {
|
||||
documentRenderers: {
|
||||
@ -44,11 +39,10 @@ export const buildCoreDocumentsDI = (env: NodeJS.ProcessEnv) => {
|
||||
fastReportTemplateResolver,
|
||||
},
|
||||
documentSigning: {
|
||||
signingService,
|
||||
signingService: restSigningService,
|
||||
signingContextResolver,
|
||||
},
|
||||
documentStorage: {
|
||||
cacheStore,
|
||||
storage,
|
||||
},
|
||||
};
|
||||
|
||||
@ -5,6 +5,7 @@ import type {
|
||||
ICompanyCertificateContext,
|
||||
ISigningContextResolver,
|
||||
} from "@erp/core/api/application";
|
||||
import type { ModuleParams } from "@erp/core/api/modules";
|
||||
|
||||
import type { ICompanySigningContextRecord } from "./company-signing-context-record.interface";
|
||||
|
||||
@ -19,26 +20,19 @@ import type { ICompanySigningContextRecord } from "./company-signing-context-rec
|
||||
export class EnvCompanySigningContextResolver implements ISigningContextResolver {
|
||||
private readonly records: Record<string, ICompanySigningContextRecord>;
|
||||
|
||||
constructor(env: NodeJS.ProcessEnv) {
|
||||
const raw = env.COMPANY_CERTIFICATES_JSON;
|
||||
|
||||
if (!raw) {
|
||||
this.records = {};
|
||||
return;
|
||||
}
|
||||
constructor(params: ModuleParams) {
|
||||
const jsonData = params.config.certificates as Record<
|
||||
string,
|
||||
{
|
||||
certificateId: string;
|
||||
certificateSecretName: string;
|
||||
certificatePasswordSecretName: string;
|
||||
}
|
||||
>;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Record<
|
||||
string,
|
||||
{
|
||||
certificateId: string;
|
||||
certificateSecretName: string;
|
||||
certificatePasswordSecretName: string;
|
||||
}
|
||||
>;
|
||||
|
||||
this.records = Object.fromEntries(
|
||||
Object.entries(parsed).map(([companySlug, cfg]) => [
|
||||
Object.entries(jsonData).map(([companySlug, cfg]) => [
|
||||
companySlug,
|
||||
{
|
||||
certificateId: cfg.certificateId,
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { IDocument } from "@erp/core/api/application";
|
||||
import type { IDocumentCacheStore } from "@erp/core/api/application/documents/services/document-cache.interface";
|
||||
|
||||
/**
|
||||
* Cache técnica de documentos firmados basada en filesystem.
|
||||
*
|
||||
* - Best-effort
|
||||
* - Nunca lanza
|
||||
* - Cachea SOLO documentos firmados
|
||||
*/
|
||||
export class FilesystemDocumentCacheStore implements IDocumentCacheStore {
|
||||
constructor(private readonly basePath: string) {}
|
||||
|
||||
async get(cacheKey: string): Promise<IDocument | null> {
|
||||
try {
|
||||
const dir = this.resolveDir(cacheKey);
|
||||
|
||||
const payload = await readFile(join(dir, "payload.bin"));
|
||||
|
||||
const meta = JSON.parse(await readFile(join(dir, "meta.json"), "utf-8"));
|
||||
|
||||
return {
|
||||
payload,
|
||||
mimeType: meta.mimeType,
|
||||
filename: meta.filename,
|
||||
};
|
||||
} catch {
|
||||
// cualquier fallo => cache miss
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async set(cacheKey: string, document: IDocument): Promise<void> {
|
||||
try {
|
||||
const dir = this.resolveDir(cacheKey);
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
||||
await writeFile(join(dir, "payload.bin"), document.payload);
|
||||
|
||||
await writeFile(
|
||||
join(dir, "meta.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
mimeType: document.mimeType,
|
||||
filename: document.filename,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
} catch {
|
||||
// best-effort: ignorar
|
||||
}
|
||||
}
|
||||
|
||||
private resolveDir(cacheKey: string): string {
|
||||
const hash = createHash("sha256").update(cacheKey).digest("hex");
|
||||
|
||||
return join(this.basePath, hash);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
DocumentStorageKeyFactory,
|
||||
type IDocument,
|
||||
type IDocumentStorage,
|
||||
} from "../../../application";
|
||||
|
||||
/**
|
||||
* Persistencia best-effort de documentos basada en filesystem.
|
||||
*
|
||||
* - Infra pura
|
||||
* - Nunca lanza
|
||||
* - No afecta al flujo del caso de uso
|
||||
*/
|
||||
export class FilesystemDocumentStorage implements IDocumentStorage {
|
||||
public constructor(private readonly basePath: string) {}
|
||||
|
||||
async existsKeyStorage(storageKey: string): Promise<Boolean> {
|
||||
try {
|
||||
const dir = this.resolveDirFromStorageKey(storageKey);
|
||||
return (await stat(dir)).isDirectory();
|
||||
} catch {
|
||||
// Consistente con saveDocument: best-effort
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async readDocument(storageKey: string) {
|
||||
try {
|
||||
const dir = this.resolveDirFromStorageKey(storageKey);
|
||||
|
||||
const payload = await readFile(path.join(dir, "document.bin"));
|
||||
const metaRaw = JSON.parse(await readFile(path.join(dir, "document.meta.json"), "utf-8"));
|
||||
|
||||
const meta = JSON.parse(metaRaw) as {
|
||||
mimeType: string;
|
||||
filename: string;
|
||||
metadata: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const document: IDocument = {
|
||||
payload,
|
||||
mimeType: meta.mimeType,
|
||||
filename: meta.filename,
|
||||
};
|
||||
|
||||
return document;
|
||||
} catch {
|
||||
// Consistente con saveDocument: best-effort
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async saveDocument(document: IDocument, metadataRecord: Record<string, unknown>): Promise<void> {
|
||||
try {
|
||||
const dir = this.resolveDirFromMetadataRecord(metadataRecord);
|
||||
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(path.join(dir, "document.bin"), document.payload);
|
||||
|
||||
await writeFile(
|
||||
path.join(dir, "document.meta.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
mimeType: document.mimeType,
|
||||
filename: document.filename,
|
||||
metadata: metadataRecord,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
} catch {
|
||||
// best-effort: ignorar errores
|
||||
}
|
||||
}
|
||||
|
||||
private resolveDirFromMetadataRecord(metadataRecord: Record<string, unknown>): string {
|
||||
/**
|
||||
* El storage NO decide claves semánticas.
|
||||
* Se limita a generar un path técnico estable.
|
||||
*/
|
||||
const storageKey = DocumentStorageKeyFactory.fromMetadataRecord(metadataRecord);
|
||||
return this.resolveDirFromStorageKey(storageKey);
|
||||
}
|
||||
|
||||
private resolveDirFromStorageKey(storageKey: string): string {
|
||||
return path.join(this.basePath, storageKey);
|
||||
}
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export * from "./filesystem-document-cache-store";
|
||||
export * from "./filesystem-signed-document-storage";
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./config";
|
||||
export * from "./database";
|
||||
export * from "./di";
|
||||
export * from "./documents";
|
||||
@ -6,4 +7,3 @@ export * from "./express";
|
||||
export * from "./logger";
|
||||
export * from "./mappers";
|
||||
export * from "./sequelize";
|
||||
export * from "./storage";
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import type { IDocument, IDocumentStorage } from "../../application";
|
||||
|
||||
/**
|
||||
* Persistencia best-effort de documentos basada en filesystem.
|
||||
*
|
||||
* - Infra pura
|
||||
* - Nunca lanza
|
||||
* - No afecta al flujo del caso de uso
|
||||
*/
|
||||
export class FilesystemDocumentStorage implements IDocumentStorage {
|
||||
public constructor(private readonly basePath: string) {}
|
||||
|
||||
async save(document: IDocument, metadata: Record<string, unknown>): Promise<void> {
|
||||
try {
|
||||
const dir = this.resolveDir(metadata);
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
||||
await writeFile(path.join(dir, "document.bin"), document.payload);
|
||||
|
||||
await writeFile(
|
||||
path.join(dir, "document.meta.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
mimeType: document.mimeType,
|
||||
filename: document.filename,
|
||||
metadata,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
} catch {
|
||||
// best-effort: ignorar errores
|
||||
}
|
||||
}
|
||||
|
||||
private resolveDir(metadata: Record<string, unknown>): string {
|
||||
/**
|
||||
* El storage NO decide claves semánticas.
|
||||
* Se limita a generar un path técnico estable.
|
||||
*/
|
||||
const hash = createHash("sha256").update(JSON.stringify(metadata)).digest("hex");
|
||||
|
||||
return path.join(this.basePath, hash);
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from "./filesystem-signed-document-storage";
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/customer-invoices",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -29,17 +29,23 @@ export class IssuedInvoiceDocumentMetadataFactory
|
||||
format: "PDF",
|
||||
languageCode: snapshot.language_code ?? "es",
|
||||
filename: this.buildFilename(snapshot),
|
||||
cacheKey: this.buildCacheKey(snapshot),
|
||||
storageKey: this.buildCacheKey(snapshot),
|
||||
};
|
||||
}
|
||||
|
||||
private buildFilename(snapshot: IssuedInvoiceReportSnapshot): string {
|
||||
// Ejemplo: factura-F2024-000123.pdf
|
||||
return `factura-${snapshot.invoice_number}.pdf`;
|
||||
// Ejemplo: factura-F2024-000123-FULANITO.pdf
|
||||
return `factura-${snapshot.series}${snapshot.invoice_number}-${snapshot.recipient.name}.pdf`;
|
||||
}
|
||||
|
||||
private buildCacheKey(snapshot: IssuedInvoiceReportSnapshot): string {
|
||||
// Versionado explícito para invalidaciones futuras
|
||||
return ["issued-invoice", snapshot.company_id, snapshot.invoice_number, "v1"].join(":");
|
||||
return [
|
||||
"issued-invoice",
|
||||
snapshot.company_id,
|
||||
snapshot.series,
|
||||
snapshot.invoice_number,
|
||||
"v1",
|
||||
].join(":");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { buildCoreDocumentsDI } from "@erp/core/api";
|
||||
import { type ModuleParams, buildCoreDocumentsDI } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
IssuedInvoiceDocumentPipelineFactory,
|
||||
type IssuedInvoiceDocumentPipelineFactoryDeps,
|
||||
} from "../documents";
|
||||
|
||||
export const buildIssuedInvoiceDocumentService = (env: NodeJS.ProcessEnv) => {
|
||||
const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(env);
|
||||
export const buildIssuedInvoiceDocumentService = (params: ModuleParams) => {
|
||||
const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params);
|
||||
|
||||
const pipelineDeps: IssuedInvoiceDocumentPipelineFactoryDeps = {
|
||||
fastReportRenderer: documentRenderers.fastReportRenderer,
|
||||
|
||||
@ -25,7 +25,7 @@ export type IssuedInvoicesInternalDeps = {
|
||||
};
|
||||
|
||||
export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInvoicesInternalDeps {
|
||||
const { database, env } = params;
|
||||
const { database } = params;
|
||||
|
||||
// Infrastructure
|
||||
const transactionManager = buildTransactionManager(database);
|
||||
@ -34,7 +34,7 @@ export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInv
|
||||
// Application helpers
|
||||
const finder = buildIssuedInvoiceFinder(repository);
|
||||
const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders();
|
||||
const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(env);
|
||||
const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(params);
|
||||
|
||||
// Internal use cases (factories)
|
||||
return {
|
||||
|
||||
@ -3,7 +3,6 @@ import {
|
||||
DocumentPostProcessorChain,
|
||||
type FastReportRenderer,
|
||||
type FastReportTemplateResolver,
|
||||
type IDocumentCacheStore,
|
||||
type IDocumentPostProcessor,
|
||||
type IDocumentSideEffect,
|
||||
type IDocumentSigningService,
|
||||
@ -34,7 +33,6 @@ export interface IssuedInvoiceDocumentPipelineFactoryDeps {
|
||||
signingContextResolver: ISigningContextResolver;
|
||||
documentSigningService: IDocumentSigningService;
|
||||
|
||||
documentCacheStore: IDocumentCacheStore;
|
||||
documentStorage: IDocumentStorage;
|
||||
}
|
||||
|
||||
@ -43,9 +41,7 @@ export class IssuedInvoiceDocumentPipelineFactory {
|
||||
deps: IssuedInvoiceDocumentPipelineFactoryDeps
|
||||
): IssuedInvoiceDocumentGeneratorService {
|
||||
// 1. Pre-processors (cache firmado)
|
||||
const preProcessors = [
|
||||
new IssuedInvoiceSignedDocumentCachePreProcessor(deps.documentCacheStore),
|
||||
];
|
||||
const preProcessors = [new IssuedInvoiceSignedDocumentCachePreProcessor(deps.documentStorage)];
|
||||
|
||||
// 2. Renderer (FastReport)
|
||||
const documentRenderer = new IssuedInvoiceDocumentRenderer(
|
||||
|
||||
@ -28,7 +28,9 @@ export class DigitalSignaturePostProcessor implements IDocumentPostProcessor {
|
||||
// 1. Resolver certificado de la empresa
|
||||
const certificate = await this.certificateResolver.resolveForCompany(metadata.companySlug);
|
||||
if (!certificate) {
|
||||
throw new Error("[DigitalSignaturePostProcessor] Compny certificate is undefined");
|
||||
throw new Error(
|
||||
`[DigitalSignaturePostProcessor] Company certificate is undefined for ${metadata.companySlug}`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Firmar payload
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import {
|
||||
DocumentCacheKeyFactory,
|
||||
DocumentStorageKeyFactory,
|
||||
type IDocument,
|
||||
type IDocumentCacheStore,
|
||||
type IDocumentMetadata,
|
||||
type IDocumentPreProcessor,
|
||||
type IDocumentStorage,
|
||||
logger,
|
||||
} from "@erp/core/api";
|
||||
|
||||
/**
|
||||
@ -15,42 +16,35 @@ import {
|
||||
* - Invalida cache corrupto
|
||||
*/
|
||||
export class IssuedInvoiceSignedDocumentCachePreProcessor implements IDocumentPreProcessor {
|
||||
constructor(private readonly cache: IDocumentCacheStore) {}
|
||||
constructor(private readonly docStorage: IDocumentStorage) {}
|
||||
|
||||
async tryResolve(metadata: IDocumentMetadata): Promise<IDocument | null> {
|
||||
if (!metadata.cacheKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadataRecord = metadata as unknown as Record<string, unknown>;
|
||||
try {
|
||||
const cacheKey = DocumentCacheKeyFactory.fromMetadata(metadata);
|
||||
const storageKey = DocumentStorageKeyFactory.fromMetadataRecord(metadataRecord);
|
||||
|
||||
return await this.cache.get(cacheKey);
|
||||
} catch {
|
||||
// best-effort: cualquier fallo se trata como cache miss
|
||||
return null;
|
||||
}
|
||||
if (!storageKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!metadata.cacheKey) {
|
||||
return null;
|
||||
}
|
||||
const exists = await this.docStorage.existsKeyStorage(storageKey);
|
||||
|
||||
try {
|
||||
const exists = await this.cache.exists(metadata.cacheKey);
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const document = await this.cache.read(metadata.cacheKey);
|
||||
const document = await this.docStorage.readDocument(storageKey);
|
||||
|
||||
if (!this.isValid(document)) {
|
||||
await this.cache.invalidate(metadata.cacheKey);
|
||||
logger.warn(`Storage key ${storageKey} not exists!`, {
|
||||
lable: "IssuedInvoiceSignedDocumentCachePreProcessor",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return document;
|
||||
} catch {
|
||||
// Cache failure → ignore and continue pipeline
|
||||
// best-effort: cualquier fallo se trata como cache miss
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -59,7 +53,9 @@ export class IssuedInvoiceSignedDocumentCachePreProcessor implements IDocumentPr
|
||||
* Validación mínima de integridad.
|
||||
* No valida firma criptográfica.
|
||||
*/
|
||||
private isValid(document: IDocument): boolean {
|
||||
private isValid(document: IDocument | null): boolean {
|
||||
if (!document) return false;
|
||||
|
||||
if (!document.payload || document.payload.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -17,11 +17,11 @@ export class PersistIssuedInvoiceDocumentSideEffect implements IDocumentSideEffe
|
||||
|
||||
async execute(document: IDocument, metadata: IDocumentMetadata): Promise<void> {
|
||||
// Si no hay cacheKey, no se persiste
|
||||
if (!metadata.cacheKey) {
|
||||
if (!metadata.storageKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Persistencia best-effort
|
||||
await this.storage.save(document, metadata);
|
||||
await this.storage.saveDocument(document, metadata as unknown as Record<string, unknown>);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
|
||||
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api";
|
||||
import type { ILogger } from "@repo/rdx-logger";
|
||||
import { type Application, type NextFunction, type Request, type Response, Router } from "express";
|
||||
import type { Sequelize } from "sequelize";
|
||||
import { type NextFunction, type Request, type Response, Router } from "express";
|
||||
|
||||
import {
|
||||
GetIssueInvoiceByIdRequestSchema,
|
||||
@ -17,12 +15,7 @@ import { ListIssuedInvoicesController } from "./controllers/list-issued-invoices
|
||||
import { ReportIssuedInvoiceController } from "./controllers/report-issued-invoice.controller";
|
||||
|
||||
export const issuedInvoicesRouter = (params: ModuleParams, deps: IssuedInvoicesInternalDeps) => {
|
||||
const { app, baseRoutePath, logger } = params as {
|
||||
app: Application;
|
||||
database: Sequelize;
|
||||
baseRoutePath: string;
|
||||
logger: ILogger;
|
||||
};
|
||||
const { app, config } = params;
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
|
||||
@ -78,5 +71,5 @@ export const issuedInvoicesRouter = (params: ModuleParams, deps: IssuedInvoicesI
|
||||
}
|
||||
);
|
||||
|
||||
app.use(`${baseRoutePath}/issued-invoices`, router);
|
||||
app.use(`${config.server.apiBasePath}/issued-invoices`, router);
|
||||
};
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
|
||||
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api";
|
||||
import type { ILogger } from "@repo/rdx-logger";
|
||||
import { type Application, type NextFunction, type Request, type Response, Router } from "express";
|
||||
import type { Sequelize } from "sequelize";
|
||||
import { type NextFunction, type Request, type Response, Router } from "express";
|
||||
|
||||
import {
|
||||
ChangeStatusProformaByIdParamsRequestSchema,
|
||||
@ -17,7 +15,7 @@ import {
|
||||
UpdateProformaByIdParamsRequestSchema,
|
||||
UpdateProformaByIdRequestSchema,
|
||||
} from "../../../../common";
|
||||
import { buildProformasDependencies } from "../../proformas-dependencies";
|
||||
import { buildProformasDependencies } from "../../di/proformas.di";
|
||||
|
||||
import {
|
||||
ChangeStatusProformaController,
|
||||
@ -31,13 +29,7 @@ import {
|
||||
} from "./controllers";
|
||||
|
||||
export const proformasRouter = (params: ModuleParams) => {
|
||||
const { app, baseRoutePath, logger } = params as {
|
||||
env: Record<string, any>;
|
||||
app: Application;
|
||||
database: Sequelize;
|
||||
baseRoutePath: string;
|
||||
logger: ILogger;
|
||||
};
|
||||
const { app, config } = params;
|
||||
|
||||
const deps = buildProformasDependencies(params);
|
||||
|
||||
@ -158,5 +150,5 @@ export const proformasRouter = (params: ModuleParams) => {
|
||||
}
|
||||
);
|
||||
|
||||
app.use(`${baseRoutePath}/proformas`, router);
|
||||
app.use(`${config.server.apiBasePath}/proformas`, router);
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/customers",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/doc-numbering",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -34,5 +34,5 @@
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
},
|
||||
"packageManager": "pnpm@10.20.0"
|
||||
"packageManager": "pnpm@10.29.3"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-criteria",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-ddd",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
import { generateUUIDv7, Result } from "@repo/rdx-utils";
|
||||
import {
|
||||
Result,
|
||||
generateUUIDv7,
|
||||
isUuidBinary,
|
||||
uuidBinaryToString,
|
||||
uuidStringToBinary,
|
||||
} from "@repo/rdx-utils";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { translateZodValidationError } from "../helpers";
|
||||
|
||||
import { ValueObject } from "./value-object";
|
||||
|
||||
export class UniqueID extends ValueObject<string> {
|
||||
@ -9,8 +17,10 @@ export class UniqueID extends ValueObject<string> {
|
||||
return schema.safeParse(value);
|
||||
}
|
||||
|
||||
static create(id?: string, generateOnEmpty = false): Result<UniqueID, Error> {
|
||||
if (!id || id?.trim() === "") {
|
||||
static create(id?: string | Buffer, generateOnEmpty = false): Result<UniqueID, Error> {
|
||||
const _id = isUuidBinary(id) ? uuidBinaryToString(id) : id;
|
||||
|
||||
if (!_id || _id?.trim() === "") {
|
||||
if (!generateOnEmpty) {
|
||||
return Result.fail(new Error("ID cannot be undefined or null"));
|
||||
}
|
||||
@ -18,7 +28,7 @@ export class UniqueID extends ValueObject<string> {
|
||||
}
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||
const valueIsValid = UniqueID.validate(id!);
|
||||
const valueIsValid = UniqueID.validate(_id!);
|
||||
|
||||
if (!valueIsValid.success) {
|
||||
return Result.fail(
|
||||
@ -44,4 +54,8 @@ export class UniqueID extends ValueObject<string> {
|
||||
toPrimitive() {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
toBuffer(): Buffer {
|
||||
return uuidStringToBinary(this.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-logger",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ILogger } from "../types";
|
||||
import type { ILogger } from "../types";
|
||||
|
||||
export class ConsoleLogger implements ILogger {
|
||||
info(message: string, meta?: any) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-utils",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,4 +1,31 @@
|
||||
import { v4 as uuidv4, v7 as uuidv7 } from "uuid";
|
||||
import {
|
||||
parse as uuidParse,
|
||||
stringify as uuidStringify,
|
||||
v4 as uuidv4,
|
||||
v7 as uuidv7,
|
||||
validate,
|
||||
version,
|
||||
} from "uuid";
|
||||
|
||||
export const generateUUIDv4 = (): string => uuidv4();
|
||||
export const generateUUIDv7 = (): string => uuidv7();
|
||||
|
||||
export function uuidStringToBinary(uuid: string): Buffer {
|
||||
return Buffer.from(uuidParse(uuid));
|
||||
}
|
||||
|
||||
export function uuidBinaryToString(buffer: Buffer): string {
|
||||
return uuidStringify(buffer);
|
||||
}
|
||||
|
||||
export function isUuidValid(value: string): boolean {
|
||||
return validate(value) && version(value) === 7;
|
||||
}
|
||||
|
||||
export function isUuidBinary(value: unknown): value is Buffer {
|
||||
return Buffer.isBuffer(value) && value.length === 16;
|
||||
}
|
||||
|
||||
export function isUuidString(value: unknown): value is string {
|
||||
return typeof value === "string" && isUuidValid(value);
|
||||
}
|
||||
|
||||
@ -53,6 +53,9 @@ importers:
|
||||
'@repo/rdx-logger':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/rdx-logger
|
||||
'@repo/rdx-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/rdx-utils
|
||||
bcrypt:
|
||||
specifier: ^5.1.1
|
||||
version: 5.1.1
|
||||
|
||||
@ -93,7 +93,6 @@ services:
|
||||
DOCUMENTS_PATH: ${DOCUMENTS_PATH}
|
||||
|
||||
SIGNED_DOCUMENTS_PATH: ${SIGNED_DOCUMENTS_PATH}
|
||||
SIGNED_DOCUMENTS_CACHE_PATH: ${SIGNED_DOCUMENTS_CACHE_PATH}
|
||||
|
||||
SIGNING_SERVICE_URL: ${SIGNING_SERVICE_URL}
|
||||
SIGNING_SERVICE_METHOD: ${SIGNING_SERVICE_METHOD}
|
||||
|
||||
@ -4,6 +4,22 @@ COMPANY_SLUG=rodax
|
||||
# Dominios
|
||||
DOMAIN=factuges.rodax-software.local
|
||||
|
||||
# API
|
||||
PORT=3002
|
||||
API_BASE_PATH=/api/v1
|
||||
FRONTEND_URL=https://factuges.rodax-software.local
|
||||
TRUST_PROXY=0
|
||||
TIMEZONE=Europe/Madrid
|
||||
API_IMAGE=factuges-server:rodax-latest
|
||||
|
||||
JWT_SECRET=supersecretkeysupersecretkeysupersecretkey
|
||||
JWT_ACCESS_EXPIRATION=1h
|
||||
JWT_REFRESH_EXPIRATION=7d
|
||||
|
||||
WARMUP_TIMEOUT_MS=10000
|
||||
WARMUP_STRICT=false
|
||||
|
||||
|
||||
# MariaDB
|
||||
DB_HOST=db
|
||||
DB_PORT=3306
|
||||
@ -13,11 +29,10 @@ DB_USER=rodax_usr
|
||||
DB_PASS=supersecret
|
||||
DB_ROOT_PASS=verysecret
|
||||
|
||||
DB_LOGGING=true
|
||||
DB_SSL=false
|
||||
DB_SYNC_MODE=alter
|
||||
|
||||
# API
|
||||
API_PORT=3002
|
||||
API_IMAGE=factuges-server:rodax-latest
|
||||
FRONTEND_URL=factuges.rodax-software.local
|
||||
|
||||
# Plantillas
|
||||
TEMPLATES_PATH=/shared/templates
|
||||
@ -28,23 +43,16 @@ DOCUMENTS_PATH=/shared/documents
|
||||
|
||||
# Firma de documentos
|
||||
SIGNED_DOCUMENTS_PATH=/shared/documents
|
||||
SIGNED_DOCUMENTS_CACHE_PATH=/shared/cache
|
||||
|
||||
SIGNING_SERVICE_URL=http://signing-service:8000/documents/sign
|
||||
SIGNING_SERVICE_METHOD=POST
|
||||
SIGNING_SERVICE_TIMEOUT_MS=15_000
|
||||
SIGNING_SERVICE_TIMEOUT_MS=15000
|
||||
SIGNING_SERVICE_MAX_RETRIES=2
|
||||
|
||||
COMPANY_CERTIFICATES_JSON='{
|
||||
"rodax": {
|
||||
"certificateId": "no se que poner aqui",
|
||||
"certificateSecretName": "certificate_secret_name",
|
||||
"certificatePasswordSecretName": "certificate_password_secret_name"
|
||||
}
|
||||
}'
|
||||
COMPANY_CERTIFICATES_PATH=/shared/company-certificates.json
|
||||
|
||||
# SYNC
|
||||
ENV = development
|
||||
ENV = production
|
||||
LOCAL_TZ = Europe/Madrid
|
||||
STATE_PATH = /app/state
|
||||
SYNC_MODE = all
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user