Audit Port and Delta Tracking
Audit Port and Delta Tracking
Relevant source files
The following files were used as context for generating this wiki page:
The audit subsystem in @primebrick/dal-pg provides a non-blocking, port-based mechanism for tracking data mutations. It automatically calculates field-level differences (deltas), stamps actor information into auditable columns, and offloads audit log persistence to an external consumer-provided implementation.
Architectural Design
The audit system is designed around the Port and Adapter pattern. The DAL defines the interface (Port), and the consuming application provides the implementation (Adapter).
Key Components
| Component | Role |
|---|---|
AuditPort | Interface defining the writeAudit method src/types/types.ts:86-88. |
AuditAction | Enum defining tracked operations: INSERT, UPDATE, SOFT_DELETE, HARD_DELETE, RESTORE src/types/types.ts:104-110. |
AuditParams | Data envelope containing the entity state, actor, and delta src/types/types.ts:91-101. |
LoggerPort | Interface for swallowing or reporting errors during fire-and-forget audit writes src/types/types.ts:113-117. |
Audit Data Flow
The following diagram illustrates how a write operation triggers the audit process.
Audit Write Pipeline
Code
Sources: src/repository/repository.ts:52-64, src/repository/repository.ts:316-335, src/types/types.ts:86-88
Delta Tracking Logic
The DAL performs field-level diffing to determine exactly what changed during an operation.
calculateDelta
This function compares the oldEntity and newEntity objects. It uses JSON.stringify to perform deep comparisons of values, ensuring that complex types like JSONB objects or arrays are correctly evaluated for changes src/repository/repository.ts:53-64.
calculateDeltaWithForcedFields
In certain scenarios (like soft-deletes), specific fields must be included in the audit log even if they haven't changed (e.g., the primary key or a status field). This helper ensures those fields are present in the delta object src/repository/repository.ts:67-79.
Entity State Comparison
Code
Sources: src/repository/repository.ts:53-79
Actor Stamping and Auditable Columns
When an entity is marked as auditable via metadata, the Repository automatically stamps the actor (provided in AuditableWriteOptions) into the corresponding PostgreSQL columns.
Automatic Column Updates
The DAL maps internal AuditableFieldType values to specific columns during write operations:
INSERT: Setscreated_by,updated_by, andcreated_at,updated_atsrc/repository/repository.ts:303-314.UPDATE: Setsupdated_byandupdated_atsrc/repository/repository.ts:405-412.SOFT_DELETE: Setsdeleted_byanddeleted_atsrc/repository/repository.ts:474-482.
Non-Blocking Execution
Audit writes are "fire-and-forget". The Repository does not await the AuditPort.writeAudit call. Instead, it attaches a .catch() block that uses the LoggerPort to log any failures without interrupting the primary database transaction or returning an error to the caller src/repository/repository.ts:316-335.
Implementation Details
The AuditPort Interface
Consumers must implement this interface to bridge the DAL to their audit logging service (e.g., an audit_logs table or an external event bus).
| Field | Type | Description |
|---|---|---|
entityClassName | string | The TS class name (e.g., "UserEntity") |
tableName | string | The physical DB table name |
entityId | number | The integer primary key of the record |
entityUuid | string | The UUID of the record |
action | AuditAction | The operation type (INSERT, UPDATE, etc.) |
delta | Record<string, {old, new}> | The map of changed fields |
Sources: src/types/types.ts:91-101
Error Swallowing with LoggerPort
If an AuditPort is provided but no LoggerPort is injected, the DAL uses a noopLogger which silently swallows errors src/repository/repository.ts:45-50. This prevents audit infrastructure issues from causing cascading failures in the primary data access layer.
Audit/Logger Association
Code
Sources: src/repository/repository.ts:148-150, src/types/types.ts:86-117