# Audit Port and Delta Tracking

# Audit Port and Delta Tracking

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [src/audit/auditable-types.ts](src/audit/auditable-types.ts)
- [src/repository/repository.ts](src/repository/repository.ts)
- [src/types/types.ts](src/types/types.ts)

</details>



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**
```mermaid
sequenceDiagram
    participant R as Repository
    participant DB as PostgreSQL
    participant AP as AuditPort (Adapter)
    participant LP as LoggerPort (Adapter)

    R->>DB: Execute Write (RETURNING *)
    DB-->>R: New Record State
    Note over R: calculateDelta(old, new)
    R->>AP: writeAudit(params) (Fire-and-Forget)
    activate AP
    Note right of R: Repository continues execution...
    AP-->>R: Promise (handled via .catch)
    deactivate AP
    alt Audit Failure
        AP--xR: Throw Error
        R->>LP: error("Audit failed", err)
    end
```
**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**
```mermaid
graph TD
    subgraph "Delta Calculation"
        A["oldEntity Record"] --> D{"calculateDelta"}
        B["newEntity Record"] --> D
        D --> E["Changed Fields Only"]
        D --> F["Forced Fields (e.g. UUID)"]
    end
    
    E & F --> G["AuditParams.delta"]
```
**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`**: Sets `created_by`, `updated_by`, and `created_at`, `updated_at` [src/repository/repository.ts:303-314]().
*   **`UPDATE`**: Sets `updated_by` and `updated_at` [src/repository/repository.ts:405-412]().
*   **`SOFT_DELETE`**: Sets `deleted_by` and `deleted_at` [src/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**
```mermaid
classDiagram
    class Repository {
        -db: Queryable
        +add(entity, data, options)
        +update(entity, updates, options)
        -calculateDelta(old, new)
    }
    class AuditPort {
        <<interface>>
        +writeAudit(params: AuditParams)
    }
    class LoggerPort {
        <<interface>>
        +error(message, args)
    }
    class AuditParams {
        +action: AuditAction
        +delta: Record
        +changedBy: string
    }

    Repository ..> AuditPort : "calls (non-blocking)"
    Repository ..> LoggerPort : "logs errors to"
    AuditPort ..> AuditParams : "receives"
```
**Sources:** [src/repository/repository.ts:148-150](), [src/types/types.ts:86-117]()

---
