769 lines
37 KiB
Markdown
769 lines
37 KiB
Markdown
**DOCUMENTO DE DISEÑO**
|
|
|
|
Sistema transversal de auditoría
|
|
|
|
**Backend ERP · Estado consolidado**
|
|
|
|
Versión de diseño: 1.0
|
|
Fecha: 24 de agosto de 2026
|
|
Estado: DISEÑO CERRADO PARA IMPLEMENTACIÓN
|
|
|
|
**Propósito
|
|
**Definir una infraestructura de auditoría portable, append-only y fácil de integrar, capaz de registrar eventos y mutaciones con contexto de operación, preservar atomicidad con las transacciones Sequelize del ERP y soportar una futura pantalla administrativa con autorización por scopes o acceso global.
|
|
|
|
# 1\. Resumen ejecutivo
|
|
|
|
La auditoría se diseña como una capacidad técnica transversal, no como un módulo funcional del ERP. El núcleo portable vive en \`@repo/rdx-audit\`; el host configura una única instancia de \`Audit\` y la distribuye por \`ModuleParams\`, igual que \`database\` y \`logger\`.
|
|
|
|
- \`IAudit.record()\` es la operación canónica. \`recordEvent()\` y \`recordMutation()\` son atajos tipados sin lógica alternativa.
|
|
- \`@repo/rdx-audit\` no depende de Company, tenant, Express, Sequelize, Criteria, roles, permisos ni del \`OperationContext\` concreto del ERP.
|
|
- El ámbito de un hecho se modela como \`scope: { type, id, label } | null\`. \`scope=null\` significa evento sin ámbito, nunca acceso global.
|
|
- La autorización de lectura es responsabilidad de la aplicación host y queda separada del scope almacenado en el evento.
|
|
- Las mutaciones auditadas en producción deben persistirse en la misma transacción Sequelize que la modificación funcional.
|
|
- \`SequelizeTransactionContext\` se introduce por DI y solo expone la transacción activa a infraestructura transversal; repositorios y casos de uso siguen recibiendo la transacción explícitamente.
|
|
- El store portable solo conoce \`append(entry)\`. \`SequelizeAuditStore\`, \`FileAuditStore\` o un store externo son adapters intercambiables.
|
|
- La lectura administrativa usa un \`SequelizeAuditReader\` server-side apoyado en \`@repo/rdx-criteria/server\`, con whitelist propia de filtros y una política de acceso separada.
|
|
- No se usa Event Sourcing. La auditoría tampoco sustituye a logs, tracing, historial de versiones de documentos ni BusinessVersion.
|
|
|
|
| **Decisión** | **Resultado** |
|
|
| ------------------------------- | -------------------------------------------------------------- |
|
|
| Arquitectura | Paquete portable + adapters host-specific |
|
|
| Persistencia productiva inicial | MariaDB/Sequelize, tablas \`audit_events\` y \`audit_changes\` |
|
|
| Mutaciones | Atómicas con la transacción funcional |
|
|
| Eventos | Pueden persistirse con o sin transacción activa según contexto |
|
|
| Lectura | Reader server-side; no forma parte del core portable |
|
|
| Autorización | Fuera de \`rdx-audit\`; puede ser scoped o unrestricted |
|
|
| Adopción | Incremental por módulo y operación |
|
|
|
|
# 2\. Objetivos y alcance
|
|
|
|
Proporcionar un Audit Trail reutilizable para registrar quién o qué realizó una acción, en qué operación, dentro de qué ámbito, sobre qué recurso y qué cambió cuando exista una mutación.
|
|
|
|
## 2.1 Objetivos específicos
|
|
|
|
- Integración mínima en cualquier módulo mediante \`IAudit\`.
|
|
- Persistencia append-only e identidad propia de cada \`AuditEntry\`.
|
|
- Modelo portable y serializable compatible con múltiples stores.
|
|
- Diff determinista para mutaciones a partir de \`before\` y \`after\` controlados por el módulo propietario.
|
|
- Contexto automático de source, operación, actor y scope.
|
|
- Atomicidad fuerte con MariaDB/Sequelize para mutaciones funcionales.
|
|
- Lectura eficiente para una futura UI administrativa con filtros, detalle y paginación.
|
|
- Diseño compatible con un administrador limitado a uno o varios scopes y con un administrador global.
|
|
|
|
## 2.2 Fuera de alcance
|
|
|
|
- Event Sourcing o reconstrucción del estado de negocio a partir del audit log.
|
|
- BusinessVersion de presupuestos, proformas u otros documentos funcionales.
|
|
- DocumentVersion para binarios o gestor documental.
|
|
- Auditar automáticamente todas las lecturas o validaciones fallidas.
|
|
- Sistema SIEM, alertas o severidades de seguridad.
|
|
- Retención legal, archivado, particionado o purge en esta primera versión.
|
|
- Búsqueda full-text sobre \`before_value\` / \`after_value\`.
|
|
- Histórico granular de arrays o líneas individuales en la primera versión.
|
|
|
|
# 3\. Terminología e invariantes
|
|
|
|
| **Concepto** | **Semántica** |
|
|
| ------------- | ---------------------------------------------------------------------------- |
|
|
| AuditEntry | Unidad histórica append-only generada por Audit. |
|
|
| Event | Hecho auditable sin diff de estado. |
|
|
| Mutation | Hecho auditable con uno o más cambios derivados de before/after. |
|
|
| Source | Sistema que produjo el registro, p.ej. \`application:uecko-erp\`. |
|
|
| Actor | Quién o qué ejecutó la acción. |
|
|
| Scope | Ámbito donde ocurrió; puede ser null. |
|
|
| Target | Recurso o categoría sobre la que ocurrió la acción. |
|
|
| Operation | Ejecución lógica local que puede producir varios AuditEntry. |
|
|
| CorrelationId | Identificador que relaciona varias operaciones, incluso entre sistemas. |
|
|
| Action | Identificador histórico estable de la operación, p.ej. \`customer.updated\`. |
|
|
|
|
**Invariante de autorización
|
|
**\`AuditEntry.scope\` describe dónde ocurrió el hecho. No expresa lo que un usuario puede consultar. \`scope=null\` nunca significa wildcard, superadmin ni acceso global.
|
|
|
|
# 4\. Arquitectura
|
|
|
|
apps/server
|
|
│
|
|
├── OperationContextStore
|
|
├── SequelizeTransactionContext
|
|
├── Audit
|
|
│ ├── IAuditContextProvider
|
|
│ └── IAuditStore
|
|
│ └── SequelizeAuditStore
|
|
│
|
|
└── initModules(ModuleParams)
|
|
├── database
|
|
├── logger
|
|
├── audit
|
|
└── transactionContext
|
|
|
|
Los módulos no declaran \`dependencies: \["audit"\]\` ni resuelven servicios \`audit:\*\`. \`audit\` es infraestructura transversal suministrada por el runtime.
|
|
|
|
## 4.1 Flujo de una mutación
|
|
|
|
HTTP / job / integration
|
|
│
|
|
▼
|
|
OperationContext
|
|
│
|
|
▼
|
|
Use Case / Application Service
|
|
│
|
|
▼
|
|
TransactionManager.complete()
|
|
│
|
|
├── TransactionContext = T1
|
|
├── repository.save(..., T1)
|
|
└── audit.recordMutation(...)
|
|
│
|
|
▼
|
|
AuditEntry
|
|
│
|
|
▼
|
|
SequelizeAuditStore
|
|
│
|
|
└── TransactionContext.current() = T1
|
|
│
|
|
▼
|
|
COMMIT o ROLLBACK
|
|
|
|
# 5\. Paquete portable \`@repo/rdx-audit\`
|
|
|
|
## 5.1 API pública
|
|
|
|
export interface IAudit {
|
|
record(input: RecordAuditInput): Promise<Result<void, AuditError>>;
|
|
recordEvent(input: RecordAuditEventShortcutInput): Promise<Result<void, AuditError>>;
|
|
recordMutation(input: RecordAuditMutationShortcutInput): Promise<Result<void, AuditError>>;
|
|
}
|
|
<br/>export interface IAuditStore {
|
|
append(entry: AuditEntry): Promise<Result<void, AuditStoreError>>;
|
|
}
|
|
|
|
\`record()\` es la única operación canónica. Los shortcuts añaden \`kind\` y delegan íntegramente en \`record()\`.
|
|
|
|
## 5.2 Inputs
|
|
|
|
export interface RecordAuditEventInput {
|
|
readonly kind: "event";
|
|
readonly action: string;
|
|
readonly target: AuditEventTarget;
|
|
}
|
|
<br/>export interface RecordAuditMutationInput {
|
|
readonly kind: "mutation";
|
|
readonly action: string;
|
|
readonly target: AuditMutationTarget;
|
|
readonly before: AuditState;
|
|
readonly after: AuditState;
|
|
readonly atomicPaths?: readonly string\[\];
|
|
}
|
|
<br/>export type RecordAuditInput =
|
|
| RecordAuditEventInput
|
|
| RecordAuditMutationInput;
|
|
|
|
## 5.3 Contratos estructurales
|
|
|
|
export interface AuditSource {
|
|
readonly type: string;
|
|
readonly id: string;
|
|
}
|
|
<br/>export interface AuditOperation {
|
|
readonly id: string;
|
|
readonly correlationId: string;
|
|
readonly origin: string;
|
|
}
|
|
<br/>export interface AuditActor {
|
|
readonly type: string;
|
|
readonly id: string | null;
|
|
readonly label: string | null;
|
|
}
|
|
<br/>export interface AuditScope {
|
|
readonly type: string;
|
|
readonly id: string;
|
|
readonly label: string | null;
|
|
}
|
|
<br/>export interface AuditEventTarget {
|
|
readonly type: string;
|
|
readonly id: string | null;
|
|
readonly label: string | null;
|
|
}
|
|
<br/>export interface AuditMutationTarget {
|
|
readonly type: string;
|
|
readonly id: string;
|
|
readonly label: string | null;
|
|
}
|
|
|
|
**Decisión KISS
|
|
**No se introduce una interfaz base común para source/actor/scope/target. La duplicación es mínima y las invariantes y evolución de cada concepto son distintas.
|
|
|
|
# 6\. \`AuditState\` y semántica de diff
|
|
|
|
export type AuditScalar = string | number | boolean | null;
|
|
export type AuditObject = Readonly<{ \[key: string\]: AuditValue }>;
|
|
export type AuditArray = readonly AuditValue\[\];
|
|
export type AuditValue = AuditScalar | AuditObject | AuditArray;
|
|
export type AuditState = Readonly<Record<string, AuditValue>>;
|
|
|
|
- No admite \`undefined\`, \`Date\`, \`Buffer\`, \`bigint\`, funciones, instancias de clase ni ciclos.
|
|
- Los números deben ser finitos.
|
|
- Las keys auditables no contienen \`.\`; el punto queda reservado para paths.
|
|
- Valores exactos como Money/Percentage/Quantity se proyectan a objetos estables con strings cuando corresponda.
|
|
- El módulo construye una proyección histórica controlada; Audit nunca introspecciona aggregates, DTOs o modelos Sequelize.
|
|
- Secrets no se materializan. Datos personales solo se incluyen cuando exista justificación histórica.
|
|
|
|
## 6.1 Reglas de diff
|
|
|
|
| **Caso** | **Regla** |
|
|
| --------------------------- | --------------------------------------------------- |
|
|
| Objetos | Comparación recursiva; el orden de keys no importa. |
|
|
| Arrays | Atómicos y order-sensitive en v1. |
|
|
| atomicPaths | Permite tratar un objeto completo como una unidad. |
|
|
| set | Campo inexistente antes y presente después. |
|
|
| unset | Campo presente antes y ausente después. |
|
|
| change | Campo presente en ambos con valores diferentes. |
|
|
| Cambio de tipo incompatible | Se registra en el path padre. |
|
|
| Orden de changes | Lexicográfico por \`path\`. |
|
|
| Mutation sin cambios | \`Result.ok()\` y no se crea \`AuditEntry\`. |
|
|
|
|
## 6.2 \`AuditChange\`
|
|
|
|
export interface AuditChange {
|
|
readonly path: string;
|
|
readonly kind: "set" | "unset" | "change";
|
|
readonly before: AuditValue | null;
|
|
readonly after: AuditValue | null;
|
|
}
|
|
|
|
# 7\. Modelo portable \`AuditEntry\`
|
|
|
|
interface AuditEntryBase {
|
|
readonly id: string;
|
|
readonly occurredAt: string;
|
|
readonly source: AuditSource;
|
|
readonly operation: AuditOperation;
|
|
readonly actor: AuditActor;
|
|
readonly scope: AuditScope | null;
|
|
readonly action: string;
|
|
}
|
|
<br/>export interface AuditEventEntry extends AuditEntryBase {
|
|
readonly kind: "event";
|
|
readonly target: AuditEventTarget;
|
|
readonly changes: readonly \[\];
|
|
}
|
|
<br/>export interface AuditMutationEntry extends AuditEntryBase {
|
|
readonly kind: "mutation";
|
|
readonly target: AuditMutationTarget;
|
|
readonly changes: readonly AuditChange\[\];
|
|
}
|
|
<br/>export type AuditEntry = AuditEventEntry | AuditMutationEntry;
|
|
|
|
- \`event\` exige \`changes=\[\]\`.
|
|
- \`mutation\` exige \`changes.length > 0\` en runtime y target identificado.
|
|
- El entry completo es serializable y portable entre MariaDB, JSONL, servicio HTTP, Kafka u otro store.
|
|
- El store nunca recibe \`before\`, \`after\`, \`atomicPaths\` ni \`OperationContext\`; recibe el \`AuditEntry\` definitivo.
|
|
|
|
# 8\. \`action\`, source, actor, scope y target
|
|
|
|
## 8.1 Action
|
|
|
|
\`action\` es un string validado y un contrato histórico estable propiedad del módulo funcional. No existe enum global ni registry.
|
|
|
|
- Convención: segmentos lowercase separados por punto, con mínimo dos segmentos.
|
|
- El primer segmento suele identificar el área o recurso propietario: \`customer.updated\`, \`proforma.issued\`, \`document.version.created\`.
|
|
- No se incluyen IDs ni datos variables en la acción.
|
|
- No se guarda \`actionLabel\`; la UI resuelve i18n y usa el identificador como fallback.
|
|
- No se persisten \`category\`, \`namespace\` ni \`severity\` en v1.
|
|
- Una acción publicada en producción no se renombra por razones estéticas; si cambia la semántica, se crea otra acción.
|
|
|
|
## 8.2 Source
|
|
|
|
Identifica el productor técnico del registro y se configura una sola vez al construir \`Audit\`. Ejemplo: \`{ type: "application", id: "uecko-erp" }\`.
|
|
|
|
## 8.3 Actor
|
|
|
|
Genérico: \`{ type, id|null, label|null }\`. El ERP puede usar \`user\`, \`system\`, \`integration\`, \`job\`, \`anonymous\`, sin que \`rdx-audit\` los enumere.
|
|
|
|
## 8.4 Scope
|
|
|
|
Genérico y opcional: company, organization, workspace, project u otro ámbito. Si existe, \`id\` es obligatorio. \`scope=null\` significa que el evento no tiene un ámbito concreto.
|
|
|
|
## 8.5 Target
|
|
|
|
Identifica la categoría/recurso afectado. Un Event puede tener \`id=null\`; una Mutation requiere un target identificado.
|
|
|
|
# 9\. Identificadores y contexto de operación
|
|
|
|
| **Identificador** | **Responsable** | **Semántica** |
|
|
| ----------------- | ------------------------ | ------------------------------------------------------- |
|
|
| AuditEntry.id | Audit | Identidad global e inmutable del registro. |
|
|
| Operation.id | OperationContext host | Identidad de una ejecución local. |
|
|
| correlationId | Contexto host / upstream | Relaciona operaciones; puede propagarse entre sistemas. |
|
|
|
|
- Los tres son strings opacos en el contrato portable.
|
|
- \`AuditEntry.id\` se genera antes del store y no usa AUTO_INCREMENT como identidad lógica.
|
|
- \`operation.id\` lo genera siempre el backend; no se confía en un ID de request aportado por cliente.
|
|
- \`correlationId\` puede recibirse de upstream si se valida; si no existe, se genera.
|
|
- \`correlationId\` es obligatorio; no se usa null.
|
|
- \`occurredAt\` lo genera Audit en ISO-8601 UTC y precisión de milisegundos.
|
|
- No se introduce \`causationId\` ni \`idempotencyKey\` en v1.
|
|
|
|
# 10\. OperationContext e \`IAuditContextProvider\`
|
|
|
|
export interface AuditContext {
|
|
readonly operation: AuditOperation;
|
|
readonly actor: AuditActor;
|
|
readonly scope: AuditScope | null;
|
|
}
|
|
<br/>export interface IAuditContextProvider {
|
|
get(): AuditContext | null;
|
|
}
|
|
|
|
\`rdx-audit\` no conoce \`AsyncLocalStorage\` ni el \`OperationContextStore\` concreto. El host adapta su contexto al puerto mínimo anterior.
|
|
|
|
- HTTP abre el contexto con actor anónimo y scope null; autenticación puede enriquecer actor y tenancy puede enriquecer scope.
|
|
- \`operationId\`, \`correlationId\` y \`origin\` son inmutables dentro de una operación.
|
|
- La transición de scope permitida es null -> A. A -> B está prohibida dentro de la misma operación.
|
|
- Jobs, integraciones y migraciones deben abrir su propio contexto explícito.
|
|
- Ausencia de contexto al auditar es fail-fast.
|
|
|
|
# 11\. \`SequelizeTransactionContext\`
|
|
|
|
Es infraestructura host-specific, independiente de \`OperationContext\` y fuera de \`@repo/rdx-audit\`. Se distribuye por DI y se implementa con \`AsyncLocalStorage\`.
|
|
|
|
export interface ISequelizeTransactionContext {
|
|
run<T>(transaction: Transaction, callback: () => T): T;
|
|
current(): Transaction | null;
|
|
}
|
|
|
|
- Los repositorios y servicios funcionales continúan recibiendo \`Transaction\` explícitamente.
|
|
- \`SequelizeTransactionManager.complete()\` abre el scope del context alrededor del callback.
|
|
- Debe conservarse el comportamiento actual: un \`Result.fail\` dentro del callback provoca rollback.
|
|
- Las transacciones anidadas quedan prohibidas inicialmente; no hay evidencia productiva que requiera savepoints o nesting.
|
|
- El contexto se inyecta; no se utiliza singleton global.
|
|
|
|
**Deuda técnica detectada
|
|
**El manager actual declara protección frente a anidamiento, pero su \`complete()\` no mantiene \`\_transaction\`; esa incoherencia debe corregirse o cubrirse con la nueva política de nesting, sin mezclar una refactorización mayor con audit.
|
|
|
|
# 12\. Stores y consistencia
|
|
|
|
## 12.1 Puerto portable
|
|
|
|
export interface IAuditStore {
|
|
append(entry: AuditEntry): Promise<Result<void, AuditStoreError>>;
|
|
}
|
|
|
|
- Solo append. No update, delete ni upsert.
|
|
- La lectura usa otro adapter; no se mezcla en el write port.
|
|
- El entry se genera completamente antes del store.
|
|
- El store debe preservar el orden de \`changes\`.
|
|
- Un fallo del store no genera a su vez otro AuditEntry.
|
|
|
|
## 12.2 Adapters previstos
|
|
|
|
| **Adapter** | **Garantía** |
|
|
| ------------------- | ---------------------------------------------------------------------- |
|
|
| SequelizeAuditStore | Store productivo principal; puede compartir la transacción funcional. |
|
|
| FileAuditStore | Útil para desarrollo/best-effort; no es ACID respecto al negocio. |
|
|
| ExternalAuditStore | Portable, pero no puede garantizar atomicidad con MariaDB por sí solo. |
|
|
| Outbox + external | Evolución futura si se necesita entrega externa fuerte sin 2PC. |
|
|
|
|
**Regla de producción
|
|
**Una configuración que exija consistencia fuerte no debe aceptar como store primario un FileAuditStore o un external store no transaccional. La validación de esa combinación pertenece al bootstrap del host, no a \`rdx-audit\`.
|
|
|
|
# 13\. \`SequelizeAuditStore\` y esquema MariaDB
|
|
|
|
La representación relacional utiliza dos tablas. No existe \`audit_change_sets\`.
|
|
|
|
audit_events
|
|
1 ─────── 0..N audit_changes
|
|
|
|
## 13.1 \`audit_events\`
|
|
|
|
| **Columna** | **Tipo propuesto** | **Nulabilidad** |
|
|
| -------------- | ------------------ | --------------- |
|
|
| id | VARCHAR(64) | NOT NULL, PK |
|
|
| kind | VARCHAR(32) | NOT NULL |
|
|
| occurred_at | DATETIME(3) | NOT NULL |
|
|
| source_type | VARCHAR(64) | NOT NULL |
|
|
| source_id | VARCHAR(128) | NOT NULL |
|
|
| operation_id | VARCHAR(128) | NOT NULL |
|
|
| correlation_id | VARCHAR(128) | NOT NULL |
|
|
| origin | VARCHAR(64) | NOT NULL |
|
|
| actor_type | VARCHAR(64) | NOT NULL |
|
|
| actor_id | VARCHAR(128) | NULL |
|
|
| actor_label | VARCHAR(255) | NULL |
|
|
| scope_type | VARCHAR(64) | NULL |
|
|
| scope_id | VARCHAR(128) | NULL |
|
|
| scope_label | VARCHAR(255) | NULL |
|
|
| action | VARCHAR(191) | NOT NULL |
|
|
| target_type | VARCHAR(64) | NOT NULL |
|
|
| target_id | VARCHAR(128) | NULL |
|
|
| target_label | VARCHAR(255) | NULL |
|
|
|
|
## 13.2 \`audit_changes\`
|
|
|
|
| **Columna** | **Tipo propuesto** | **Notas** |
|
|
| -------------- | ------------------ | ------------------------ |
|
|
| audit_event_id | VARCHAR(64) | PK/FK -> audit_events.id |
|
|
| position | INT UNSIGNED | PK; preserva orden |
|
|
| path | VARCHAR(255) | NOT NULL |
|
|
| kind | VARCHAR(32) | set/unset/change |
|
|
| before_value | JSON | NULL |
|
|
| after_value | JSON | NULL |
|
|
|
|
- MariaDB \`JSON\` es adecuado para presentar valores históricos; no se diseña como mecanismo principal de búsqueda.
|
|
- No existen FKs a Company, User, Customer, Proforma u otros recursos funcionales.
|
|
- \`audit_changes -> audit_events\` sí tiene integridad referencial interna; \`ON DELETE CASCADE\` puede reservarse para retención administrativa futura.
|
|
- No se usan timestamps Sequelize automáticos \`createdAt/updatedAt\`.
|
|
- No se añade \`stored_at\`, soft delete, particionado, metadata genérica ni índices JSON en v1.
|
|
|
|
## 13.3 Constraints
|
|
|
|
- \`audit_events.kind IN (event, mutation)\`.
|
|
- \`audit_changes.kind IN (set, unset, change)\`.
|
|
- Scope: o todos sus campos son null, o al menos \`scope_type\` y \`scope_id\` están presentes.
|
|
- \`kind=mutation\` implica \`target_id IS NOT NULL\`.
|
|
- Las invariantes se validan también antes de persistir; las CHECK constraints son defensa adicional.
|
|
|
|
## 13.4 Índices iniciales
|
|
|
|
PRIMARY KEY (id)
|
|
INDEX (occurred_at, id)
|
|
INDEX (scope_type, scope_id, occurred_at, id)
|
|
INDEX (target_type, target_id, occurred_at, id)
|
|
INDEX (actor_type, actor_id, occurred_at, id)
|
|
INDEX (operation_id)
|
|
INDEX (correlation_id)
|
|
|
|
No se indexan inicialmente \`kind\`, \`path\`, JSON values, source ni action. Se añadirán solo si las consultas reales justifican el coste de escritura.
|
|
|
|
# 14\. Lectura administrativa
|
|
|
|
\`IAuditReader\` no forma parte del core portable porque el modelo actual de Criteria se exporta desde \`@repo/rdx-criteria/server\` y arrastra Sequelize. La lectura se implementa en infraestructura server-side.
|
|
|
|
interface IAuditReader {
|
|
search(
|
|
criteria: Criteria,
|
|
access: AuditReadAccess,
|
|
): Promise<Result<AuditSearchResult, Error>>;
|
|
<br/>findById(
|
|
id: string,
|
|
access: AuditReadAccess,
|
|
): Promise<Result<AuditEntry | null, Error>>;
|
|
}
|
|
|
|
## 14.1 \`AuditReadAccess\`
|
|
|
|
type AuditReadAccess =
|
|
| Readonly<{ mode: "unrestricted" }>
|
|
| Readonly<{
|
|
mode: "scoped";
|
|
scopes: readonly AuditScopeIdentity\[\];
|
|
includeUnscoped: boolean;
|
|
}>;
|
|
<br/>interface AuditScopeIdentity {
|
|
readonly type: string;
|
|
readonly id: string;
|
|
}
|
|
|
|
- Administrador limitado: \`mode=scoped\`, uno o varios scopes autorizados.
|
|
- Administrador global: \`mode=unrestricted\` y no se añade predicado de scope.
|
|
- \`includeUnscoped\` es explícito; no se infiere.
|
|
- El access model no utiliza \`OperationContext.scope\` como autorización automática.
|
|
|
|
## 14.2 Criteria
|
|
|
|
- Se reutiliza \`@repo/rdx-criteria/server\` solo dentro del reader Sequelize.
|
|
- El reader mantiene whitelist propia de campos y operadores porque el converter actual puede utilizar un field desconocido como columna Sequelize.
|
|
- Los mappings lógico -> columna son explícitos para todos los fields auditables.
|
|
- \`strictMode\` se activa para sorting, pero no sustituye la whitelist de filtros.
|
|
- Paginación inicial: \`pageNumber/pageSize\`, zero-based, coherente con el backend actual.
|
|
- Orden por defecto: \`occurredAt DESC\`; el reader añade \`id DESC\` como desempate técnico.
|
|
- El máximo de \`pageSize\` se valida en el boundary porque el MAX actual no está aplicado de forma global.
|
|
|
|
## 14.3 Filtros iniciales
|
|
|
|
| **Campo** | **Operadores previstos** |
|
|
| -------------------------------- | ---------------------------------- |
|
|
| id | EQUALS |
|
|
| kind | EQUALS |
|
|
| occurredAt | EQUALS, >, >=, <, <= |
|
|
| sourceType/sourceId | EQUALS |
|
|
| operationId/correlationId/origin | EQUALS |
|
|
| actorType/actorId | EQUALS; null checks cuando aplique |
|
|
| scopeType/scopeId | EQUALS, IS_NULL, IS_NOT_NULL |
|
|
| action | EQUALS; búsqueda libre separada |
|
|
| targetType/targetId | EQUALS; null checks cuando aplique |
|
|
|
|
## 14.4 Quick search
|
|
|
|
Primera versión: quick search específico con LIKE sobre \`action\`, \`actor_label\`, \`scope_label\` y \`target_label\`. No se crean FULLTEXT indexes ni se busca dentro de JSON en v1.
|
|
|
|
## 14.5 Limitaciones actuales de Criteria
|
|
|
|
- No hay \`IN\`, \`STARTS_WITH\` ni múltiples órdenes expresables en Criteria.
|
|
- La autorización multi-scope se construye directamente como \`WhereOptions\`, no mediante Criteria.
|
|
- El prefijo \`customer.\*\` no es requisito de v1; si aparece necesidad real puede añadirse \`STARTS_WITH\` como extensión genérica.
|
|
- Existe una incoherencia \`NOT_EQUALS\` / \`NOT_EQUAL\` en \`rdx-criteria\`; se registra como deuda técnica independiente.
|
|
|
|
# 15\. Futura API/UI administrativa
|
|
|
|
La pantalla aún no está definida, pero el backend queda preparado para list/detail sin comprometer la autorización futura.
|
|
|
|
GET /admin/audit?...criteria...
|
|
GET /admin/audit/:audit_event_id
|
|
|
|
Las rutas son orientativas; la ubicación definitiva se decidirá al diseñar el módulo de administración.
|
|
|
|
- El listado consulta solo \`audit_events\` y devuelve un summary sin \`changes\`.
|
|
- El detalle consulta \`audit_events\` con \`id + access\` en la misma query y después carga \`audit_changes\` ordenados por position.
|
|
- No se realiza un \`findById\` unrestricted seguido de verificación posterior.
|
|
- Los HTTP DTOs siguen separados del modelo portable \`AuditEntry\` y se validan con los patrones DTO/Zod vigentes del ERP.
|
|
- La UI resuelve labels de acciones mediante i18n y usa el action estable como fallback.
|
|
|
|
# 16\. Integración con \`ModuleParams\` y módulos
|
|
|
|
interface ModuleParams {
|
|
// contratos actuales...
|
|
readonly audit: IAudit;
|
|
readonly transactionContext: ISequelizeTransactionContext;
|
|
}
|
|
|
|
No se acomete ahora un refactor general del tipado débil de \`ModuleParams\`; audit solo añade las dependencias necesarias.
|
|
|
|
- \`transactionContext\` se pasa a los 14 callers actuales de \`buildTransactionManager\` mediante DI explícita.
|
|
- \`audit\` solo se inyecta en use cases/application services que auditan operaciones.
|
|
- No se inyecta en repositories, mappers, domain entities ni controllers.
|
|
- Cross-module services que posean una acción auditada reciben \`IAudit\` en su propio composition root; el caller no pasa audit como argumento.
|
|
- Cada action tiene un único owner funcional para evitar duplicados.
|
|
|
|
# 17\. Patrones normativos por operación
|
|
|
|
| **Operación** | **before** | **after** | **Action** |
|
|
| ----------------- | --------------- | ----------------- | --------------------------------------------- |
|
|
| Create | {} | Estado persistido | \`\*.created\` |
|
|
| Update ordinario | Estado anterior | Estado nuevo | \`\*.updated\` |
|
|
| Update semántico | Estado anterior | Estado nuevo | Acción propia, p.ej. \`customer.deactivated\` |
|
|
| Delete físico | Estado anterior | {} | \`\*.deleted\` |
|
|
| Evento no mutante | — | — | \`recordEvent()\` |
|
|
|
|
## 17.1 Secuencia de mutation
|
|
|
|
load aggregate
|
|
\-> build BEFORE AuditState
|
|
\-> apply domain mutation
|
|
\-> persist
|
|
\-> build AFTER AuditState
|
|
\-> audit.recordMutation()
|
|
\-> commit
|
|
|
|
**Orden obligatorio
|
|
**Audit se registra después de que la persistencia funcional haya sido aceptada, pero antes de abandonar el callback transaccional. Si el audit falla, la transacción completa debe hacer rollback.
|
|
|
|
## 17.2 Proyección por módulo
|
|
|
|
- No se usa \`aggregate.toPrimitives()\` automáticamente.
|
|
- Cada módulo define una proyección histórica explícita y determinista.
|
|
- Referencias legibles pueden representarse como \`{ id, label }\` y marcarse en \`atomicPaths\`.
|
|
- Arrays se normalizan en el módulo cuando su orden no sea semántico.
|
|
- Create usa \`before={}\` para obtener un diff de campos establecidos.
|
|
- Delete captura target y before antes de borrar físicamente.
|
|
|
|
# 18\. Errores y atomicidad
|
|
|
|
- \`recordMutation()\` y \`recordEvent()\` devuelven \`Result\`; no son fire-and-forget.
|
|
- El use case propaga \`Result.fail\` sin exponer detalles SQL.
|
|
- En una mutation productiva: audit failure -> use case failure -> \`TransactionManager.complete()\` detecta Result.fail -> rollback.
|
|
- \`SequelizeAuditStore\` requiere transacción activa para mutations bajo política strong/required; no abre silenciosamente otra transacción.
|
|
- Un Event puede reutilizar una transacción activa o realizar una escritura autónoma si la política host lo permite.
|
|
- \`rdx-audit\` no contiene flags \`required/bestEffort\`; la combinación store/consistency se valida al construir el runtime.
|
|
|
|
# 19\. Seguridad, privacidad y observabilidad
|
|
|
|
- No se almacenan passwords, tokens, secretos, claves privadas ni credenciales.
|
|
- PII se limita a valores que tengan utilidad histórica justificada; labels son snapshots auxiliares, no descripciones completas.
|
|
- No existen FKs a recursos funcionales para preservar el registro histórico aunque el recurso cambie o desaparezca.
|
|
- La auditoría no sustituye al logging. Reads, validation failures y errores técnicos se observan mediante logs/tracing salvo eventos explícitos de negocio/seguridad.
|
|
- No se persisten roles/permisos del actor en cada entry. La futura autorización de lectura usa el estado de autorización del host.
|
|
- El usuario DB de runtime debería aplicar menor privilegio cuando la infraestructura general lo permita; no se introducen triggers anti-update en v1.
|
|
|
|
# 20\. Estrategia de pruebas
|
|
|
|
## 20.1 \`rdx-audit\`
|
|
|
|
- Validación de AuditState y rechazo de valores no portables.
|
|
- Diff recursivo de objetos, arrays atómicos, atomicPaths y orden lexicográfico.
|
|
- Event siempre con changes vacío.
|
|
- Mutation sin cambios devuelve éxito sin append.
|
|
- Mutation con target no identificado se rechaza.
|
|
- Context inexistente produce error fail-fast.
|
|
- IDs y clock deterministas en tests mediante dependencias sustituibles si se introducen.
|
|
|
|
## 20.2 TransactionContext
|
|
|
|
- \`current()\` es null fuera de \`complete()\`.
|
|
- Dentro de \`complete()\` devuelve la misma Transaction que recibe el callback.
|
|
- Restauración del scope al finalizar y también ante error.
|
|
- \`Result.fail\` sigue provocando rollback.
|
|
- Nesting prohibido se detecta explícitamente.
|
|
|
|
## 20.3 SequelizeAuditStore
|
|
|
|
- Event inserta solo \`audit_events\`.
|
|
- Mutation inserta event + changes de forma atómica.
|
|
- Fallos en changes provocan rollback completo.
|
|
- Mutation sin transaction activa bajo política required falla.
|
|
- Mapping JSON y orden \`position\` son correctos.
|
|
|
|
## 20.4 Módulos consumidores
|
|
|
|
- Verificar action, target, before y after en las operaciones instrumentadas.
|
|
- Los unit tests mockean/fakean \`IAudit\`; no necesitan conocer store, OperationContext ni TransactionContext.
|
|
- La atomicidad completa se prueba en infraestructura y en uno o dos flujos representativos, no repetida en cada use case.
|
|
|
|
# 21\. Plan de implementación incremental
|
|
|
|
1. Crear \`@repo/rdx-audit\` con tipos portables, \`Audit\`, validación y diff.
|
|
2. Introducir \`OperationContext\`/adapter \`IAuditContextProvider\` según el contrato acordado.
|
|
3. Crear \`ISequelizeTransactionContext\` e implementación con AsyncLocalStorage.
|
|
4. Adaptar \`SequelizeTransactionManager.complete()\` conservando rollback de \`Result.fail\` y política de no nesting.
|
|
5. Construir una única instancia de transaction context en \`apps/server\` y pasarla por \`ModuleParams\`/runtime de tests.
|
|
6. Actualizar los 14 callers de \`buildTransactionManager\`.
|
|
7. Implementar modelos, migración y \`SequelizeAuditStore\`.
|
|
8. Configurar \`Audit\` una vez en \`apps/server\` y pasarlo a módulos.
|
|
9. Instrumentar un módulo piloto con create/update/status change y validar ergonomía.
|
|
10. Extender progresivamente la cobertura a payment terms, customers, proformas, issued invoices y resto.
|
|
11. Implementar \`SequelizeAuditReader\` y autorización cuando se diseñe la pantalla administrativa.
|
|
|
|
# 22\. Cobertura y ownership por módulo
|
|
|
|
La adopción es explícita. Un módulo solo se considera cubierto cuando se documentan sus operaciones mutables auditadas.
|
|
|
|
Customers audit coverage
|
|
\[x\] create
|
|
\[x\] update
|
|
\[x\] deactivate
|
|
\[x\] reactivate
|
|
\[ \] delete - no existe
|
|
|
|
No se crea un registry global de acciones. El owner funcional puede usar constantes locales para evitar typos cuando haya varias acciones.
|
|
|
|
# 23\. Alternativas descartadas
|
|
|
|
| **Alternativa** | **Motivo de descarte** |
|
|
| ------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
|
| \`modules/audit\` + service registry | Audit es infraestructura transversal; añadiría wiring funcional artificial. |
|
|
| \`record(input, transaction)\` | Filtra semántica Sequelize al contrato portable y empeora stores alternativos. |
|
|
| Transaction context singleton | Reduce 14 cambios a costa de estado global oculto y peor testabilidad. |
|
|
| Audit Sequelize hooks | No conocen adecuadamente la semántica funcional y pueden duplicar/registrar cambios no deseados. |
|
|
| Audit desde Domain | Acopla dominio a infraestructura transversal. |
|
|
| Audit desde controllers | No posee la semántica ni atomicidad funcional completa. |
|
|
| Criteria dentro de \`rdx-audit\` | El Criteria actual server-side depende de Sequelize; duplicar DSL tampoco aporta valor. |
|
|
| \`scope=null\` = acceso global | Mezcla dato histórico con autorización y hace ambiguas las consultas. |
|
|
| Arrays granulares v1 | Complejidad innecesaria; arrays atómicos cubren el alcance inicial. |
|
|
| Event Sourcing | No es requisito y cambiaría radicalmente el modelo de persistencia funcional. |
|
|
|
|
# 24\. Evoluciones futuras, no bloqueantes
|
|
|
|
- \`IAuditReader\` con cursor/keyset si el volumen hace costoso el offset.
|
|
- Operadores genéricos \`STARTS_WITH\`, \`IN\` y orden múltiple en \`rdx-criteria\` si aparecen casos reales.
|
|
- Full-text o índices adicionales según métricas reales de la UI administrativa.
|
|
- Retención, purge administrativo, exportación y archivado.
|
|
- Outbox para envío fiable a store externo.
|
|
- Diff granular de Collections/arrays con keys estables si aporta valor.
|
|
- \`causationId\` en flujos event-driven futuros.
|
|
- \`source.version\` o environment si un mismo store mezcla despliegues/sistemas y aparece necesidad operativa.
|
|
- BusinessVersion y DocumentVersion como capacidades separadas, no absorbidas por Audit.
|
|
|
|
# 25\. Checklist de decisiones cerradas
|
|
|
|
| **Tema** | **Decisión** |
|
|
| ------------------- | ------------------------------------------------------- |
|
|
| Core portable | @repo/rdx-audit |
|
|
| IAudit | record + recordEvent + recordMutation |
|
|
| Store | append-only |
|
|
| Scope | genérico; null permitido |
|
|
| Autorización | host-specific y separada |
|
|
| Transacción | ambient solo para infraestructura; explícita en negocio |
|
|
| Transaction context | DI, no singleton |
|
|
| Nesting | prohibido inicialmente |
|
|
| Persistencia SQL | audit_events + audit_changes |
|
|
| Reader | server-side con Criteria |
|
|
| Arrays | atómicos v1 |
|
|
| Create | before={} |
|
|
| Delete | after={} |
|
|
| No-op mutation | no AuditEntry |
|
|
| Lecturas | no audit por defecto |
|
|
| Adopción | incremental |
|
|
|
|
# Anexo A. Ejemplo de integración
|
|
|
|
return this.transactionManager.complete(async transaction => {
|
|
const customerResult = await this.customerRepository.findById(
|
|
customerId,
|
|
transaction,
|
|
);
|
|
<br/>if (customerResult.isFailure) {
|
|
return customerResult;
|
|
}
|
|
<br/>const customer = customerResult.value;
|
|
const before = toCustomerAuditState(customer);
|
|
<br/>const updateResult = customer.update(input);
|
|
if (updateResult.isFailure) {
|
|
return updateResult;
|
|
}
|
|
<br/>const saveResult = await this.customerRepository.save(
|
|
customer,
|
|
transaction,
|
|
);
|
|
<br/>if (saveResult.isFailure) {
|
|
return saveResult;
|
|
}
|
|
<br/>const auditResult = await this.audit.recordMutation({
|
|
action: "customer.updated",
|
|
target: toCustomerAuditTarget(customer),
|
|
before,
|
|
after: toCustomerAuditState(customer),
|
|
});
|
|
<br/>if (auditResult.isFailure) {
|
|
return auditResult;
|
|
}
|
|
<br/>return Result.ok(customer);
|
|
});
|
|
|
|
# Anexo B. Ejemplo de \`AuditEntry\`
|
|
|
|
{
|
|
"id": "01J...",
|
|
"kind": "mutation",
|
|
"occurredAt": "2026-08-24T17:20:10.123Z",
|
|
"source": { "type": "application", "id": "uecko-erp" },
|
|
"operation": {
|
|
"id": "OP1",
|
|
"correlationId": "CORR1",
|
|
"origin": "http"
|
|
},
|
|
"actor": {
|
|
"type": "user",
|
|
"id": "U1",
|
|
"label": "<admin@example.es>"
|
|
},
|
|
"scope": {
|
|
"type": "company",
|
|
"id": "C1",
|
|
"label": "Empresa A"
|
|
},
|
|
"action": "customer.updated",
|
|
"target": {
|
|
"type": "customer",
|
|
"id": "CU1",
|
|
"label": "ACME"
|
|
},
|
|
"changes": \[
|
|
{
|
|
"path": "email",
|
|
"kind": "change",
|
|
"before": "<old@acme.es>",
|
|
"after": "<new@acme.es>"
|
|
}
|
|
\]
|
|
} |