# Write Operations

# Write Operations

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

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

- [docs/ai/dal-usage-guide.md](docs/ai/dal-usage-guide.md)
- [src/repository/repository.ts](src/repository/repository.ts)
- [test/repository-crud.test.ts](test/repository-crud.test.ts)

</details>



The `Repository` class provides a set of methods for single-row modifications. These operations are designed with a "safety-first" philosophy, featuring automatic audit stamping, version incrementing, and mandatory `RETURNING *` hydration to ensure the JavaScript entity remains synchronized with the database state.

## Overview of Write Lifecycle

Every write operation in the DAL follows a structured lifecycle to maintain data integrity and auditability:

1.  **Metadata Resolution**: The system inspects the `@Entity` for primary keys and auditable fields [src/repository/repository.ts:159-161]().
2.  **Actor Stamping**: If the entity is auditable, the `actor` provided in `AuditableWriteOptions` is applied to `created_by` or `updated_by` [src/repository/repository.ts:316-324]().
3.  **SQL Generation**: Parameterized SQL is built, targeting either the primary key or a specific column defined by `matchBy` [src/repository/repository.ts:100-116]().
4.  **Execution & Hydration**: The query is executed with `RETURNING *`, and the resulting row is hydrated back into the JS entity type [src/repository/repository.ts:281-285]().
5.  **Audit Dispatch**: If an `AuditPort` is configured, a delta is calculated and the audit event is fired [src/repository/repository.ts:335-343]().

### Data Flow: Single-Row Write
The following diagram illustrates how a write request flows from the `Repository` through metadata resolution to SQL execution.

**Write Operation Flow**
```mermaid
graph TD
    subgraph "Repository API"
        A["repo.add() / repo.update()"]
    end

    subgraph "Metadata Engine"
        B["getEntityPersistenceMeta()"]
        C["resolveMatchColumn()"]
    end

    subgraph "Audit Subsystem"
        D["AuditableWriteOptions (actor)"]
        E["calculateDelta()"]
        F["AuditPort.fire()"]
    end

    subgraph "Database"
        G["SQL Execution (RETURNING *)"]
    end

    A --> B
    B --> C
    A --> D
    D --> G
    G --> E
    E --> F
```
Sources: [src/repository/repository.ts:148-150](), [src/repository/repository.ts:95-116](), [src/repository/repository.ts:335-343](), [src/meta/entity-meta.ts:60-65]()

## Core Write Methods

### `add` (Insert)
Inserts a new record. It automatically handles `uuid` generation (if defined), `version` initialization to `1`, and audit stamping for `created_by` and `updated_by`.

*   **Logic**: Validates that at least one column is being inserted [src/repository/repository.ts:273-275]().
*   **Audit**: Stamps `created_at`, `updated_at`, `created_by`, and `updated_by` if the entity supports them [src/repository/repository.ts:316-324]().

### `update` (Update by Key)
Updates a record by matching a specific column (defaulting to the `@Key`).

*   **Match Logic**: Uses `matchBy` from `MatchByOptions` to determine the `WHERE` clause. The match value is extracted from the update object and removed from the `SET` clause [src/repository/repository.ts:122-135]().
*   **Concurrency**: Automatically increments the `version` column if present [src/repository/repository.ts:468-470]().
*   **Audit**: Calculates a delta between the old and new record and dispatches it to the `AuditPort` [src/repository/repository.ts:494-502]().

### `upsert` (Insert on Conflict)
Performs an `INSERT ... ON CONFLICT` operation.

*   **Conflict Target**: Uses the primary key as the conflict target [src/repository/repository.ts:384-386]().
*   **Behavior**: If a conflict occurs, it updates the existing record, increments the version, and updates the `updated_by`/`updated_at` fields [src/repository/repository.ts:405-410]().

### `delete` and `restore` (Soft-Delete)
The DAL favors soft-deletion over physical deletion for auditable entities.

*   **delete**: Sets `deleted_at` to the current timestamp and `deleted_by` to the provided actor [src/repository/repository.ts:553-561]().
*   **restore**: Reverses a soft-delete by setting `deleted_at` and `deleted_by` back to `null` [src/repository/repository.ts:603-608]().

### `hardDelete` (Physical Delete)
Performs a physical `DELETE FROM` query. This is irreversible and should be used with caution [src/repository/repository.ts:646-654]().

Sources: [src/repository/repository.ts:264-353](), [src/repository/repository.ts:370-435](), [src/repository/repository.ts:446-519](), [src/repository/repository.ts:532-583](), [src/repository/repository.ts:594-633](), [src/repository/repository.ts:646-668]()

## Code Entity Mapping

This diagram maps natural language operations to the specific internal functions and classes responsible for executing them.

**Entity Mapping: Natural Language to Code**
```mermaid
classDiagram
    class Repository {
        +add(entity, data, options)
        +update(entity, updates, options)
        +upsert(entity, data, options)
        +delete(entity, match, options)
    }

    class InternalHelpers {
        +resolveMatchColumn()
        +extractMatchValue()
        +calculateDelta()
    }

    class MetadataDecorators {
        +@Key()
        +@AuditableField()
        +@DeletableField()
    }

    Repository ..> InternalHelpers : uses
    Repository ..> MetadataDecorators : inspects
```
Sources: [src/repository/repository.ts:148-150](), [src/repository/repository.ts:95-116](), [src/repository/repository.ts:53-64](), [src/meta/entity-decorators.ts:74-84]()

## Write Options and Auditing

### `AuditableWriteOptions`
All write methods accept an options object extending `AuditableWriteOptions`.

| Option | Type | Description |
| :--- | :--- | :--- |
| `actor` | `string` | **Required.** The identity of the user/system performing the write. |
| `auditPort` | `AuditPort` | Optional override for the global audit port. |
| `matchBy` | `string` | For updates/deletes: specify which property to use in the `WHERE` clause. |

### Delta Calculation
When an update occurs, the `Repository` uses `calculateDelta` to compare the `oldEntity` (fetched via the `RETURNING` clause or prior state) with the `newEntity` [src/repository/repository.ts:53-64]().

```typescript
function calculateDelta(
  oldEntity: Record<string, unknown>,
  newEntity: Record<string, unknown>
): Record<string, { old: unknown; new: unknown }> {
  const delta: Record<string, { old: unknown; new: unknown }> = {};
  for (const key in newEntity) {
    if (JSON.stringify(oldEntity[key]) !== JSON.stringify(newEntity[key])) {
      delta[key] = { old: oldEntity[key], new: newEntity[key] };
    }
  }
  return delta;
}
```
Sources: [src/repository/repository.ts:53-64](), [src/types/types.ts:106-115]()

## Error Handling in Writes

Write operations may throw the following DAL-specific errors:

*   **`NotFoundError`**: Thrown during `update` or `delete` if the `matchBy` criteria finds no rows [src/repository/repository.ts:485-487]().
*   **`ValidationError`**: Thrown if an `add` operation contains no columns or if a `matchBy` value is missing [src/repository/repository.ts:274-276](), [src/repository/repository.ts:128-131]().
*   **`UnknownColumnError`**: Thrown if the `matchBy` property does not exist on the entity [src/repository/repository.ts:103-105]().

Sources: [src/errors/errors.ts:1-25](), [src/repository/repository.ts:100-116]()

---
