# Keep a record of who changed what

An entity keeps no history of its changes unless you ask. Give it an `audit:`
section and Jadpo writes down each change in the same transaction: who made it,
when, through which action and request, and each changed field's value before
and after. You write no logging code, and nothing can change a record without
its entry. This guide keeps the history of a book-keeper's invoices and reads it
back through a route.

You start from an app with a `Clerk` entity that people sign in as, an `Invoice`
entity, and actions called `hire_clerk`, `issue_invoice` and `annotate_invoice`
that change them. [Add sign-in](/guides/guide-add-sign-in.md) and
[Store and change a record](/guides/guide-store-and-change-a-record.md) show how to write
those.

## Turn it on

`audit:` goes on the entity. `retention:` says how long each entry is kept,
counted from when it was written, and takes a [`Duration`](/docs/types/type-duration.md) setting as every period
does. Write `audit: {}` to keep each entry until its record is erased.

`config.jadpo`:

```jadpo
config {
    /// How long each invoice change is kept: seven years.
    audit_retention: Duration {
        default: 2557d
    }
}
```

`domains/billing/entities/invoice.jadpo`. `audit:` is new:

```jadpo
use billing/entities { Clerk }
use billing/types { InvoiceNote }

/// A bill, whose every change is kept for seven years.
entity Invoice {
    id: Uuid
    clerk_id: Clerk.id {
        on_delete: OnDelete.restricted
    }
    number: InvoiceNumber
    note: InvoiceNote?
    discount: Int {
        policy: {
            read: [clerk_id]
            update: [clerk_id]
        }
    }

    policy: {
        create: [clerk_id]
        read: [Access.authenticated]
        update: [clerk_id]
    }

    audit: {
        retention: {
            after: config.audit_retention
        }
    }
}
```

The entries live in a table Jadpo owns, `jadpo_audit`, in the same store as the
record, so make a migration:

```sh
jadpo migrate new audit_invoices
```

From now on, every write to an invoice adds an entry. Nothing in your code can
change or remove one. A field no request may touch, `FieldAccess.server_only`, is
left out.

An audited entity can't hold an [`Email`](/docs/types/type-email.md), a field marked `personal:`, or a `File`
field yet. Keep them on an entity with no `audit:`, such as a `Customer` that the
invoice refers to. The last section of this page says more.

## Read the history

`Invoice.history(invoice_id)` is a query Jadpo generates. It gives one page of 25
entries, newest first, and its `next` is the cursor for the page after. Each is
an `AuditEntry`:

| Field | Holds |
|---|---|
| `at` | When the change was made |
| `change` | `AuditChange.created`, `.updated` or `.deleted` |
| `by` | `AuditBy.user`, `.service`, `.application` (a schedule or a subscriber) or `.no_caller` (a test's, a seed's or a tool's call without a caller) |
| `user_id`, `service_id` | The signed-in user's or service's `id`, or `none` |
| `action` | The outermost action the change was made through, by name |
| `request` | The request's id, when a request made the change |
| `trace` | The trace id it was made in, which a subscriber keeps from the request that emitted its event |
| `fields` | Each changed field: its name, `field`, and its text `before` and `after`, `none` where it had no value |

A history is read as the caller may see it. It needs the `read` grant on the
record and leaves out the fields the caller can't read, so here another clerk
sees each change without the `discount`, which only the invoice's clerk may read.
A caller who may not read the record gets an empty page.

## Send it through a route

A route can't send an `AuditEntry` as it is, because it isn't an `Output`. You
build an `Output` of your own, which also makes you choose what your API's clients
see.

`domains/billing/types/change_line.jadpo`:

```jadpo
/// One field an invoice's change touched.
type ChangeLine = Output {
    field: Text
    before: Text?
    after: Text?
}
```

`domains/billing/types/invoice_change.jadpo`:

```jadpo
use billing/types { ChangeLine }

/// One change to an invoice, as the API sends it.
type InvoiceChange = Output {
    at: Instant
    action: Text?
    user_id: Uuid?
    fields: List<ChangeLine>
}
```

`domains/billing/actions/invoice_changes.jadpo`:

```jadpo
use billing/entities { Invoice }
use billing/types { ChangeLine, InvoiceChange }

/// The invoice's latest changes, newest first, as the caller may see them.
action invoice_changes(me: Principal.user, invoice_id: Invoice.id) -> List<InvoiceChange> {
    var page: Page<AuditEntry> = Invoice.history(invoice_id)
    return list.map(
        page.items,
        entry -> InvoiceChange {
            at: entry.at
            action: entry.action
            user_id: entry.user_id
            fields: list.map(
                entry.fields,
                line -> ChangeLine {
                    field: line.field
                    before: line.before
                    after: line.after
                }
            )
        }
    )
}
```

`domains/billing/routes/invoices.jadpo`:

```jadpo
use billing/entities { Invoice }
use billing/types { InvoiceChange }
use billing/actions { invoice_changes }

/// The office's invoices.
routes Invoices {
    prefix: /invoices

    /// One invoice's latest changes, newest first.
    GET /{invoice_id: Invoice.id}/changes {
        output: List<InvoiceChange>
        run: invoice_changes(current_principal.user, invoice_id)
    }
}
```

Run `jadpo api update` to bring the API record up to date. This route gives the
latest 25 changes. For more, take a `PageCursor` from the caller and pass it as
`Invoice.history(invoice_id, after: after)`.

## Test it

These tests make changes through the actions, as a person would, and read them
back.

`domains/billing/actions/invoice_changes.test.jadpo`:

```jadpo
use billing/entities { Clerk, Invoice }
use billing/types { InvoiceChange }
use billing/actions { annotate_invoice, hire_clerk, issue_invoice }

test "each change to an invoice comes back newest first" {
    var ada: Clerk = attempt hire_clerk("ada")
    var invoice: Invoice = issue_invoice(Principal.user(ada), "INV-1")
    attempt annotate_invoice(Principal.user(ada), invoice.id, "Paid by card")
    var changes: List<InvoiceChange> = invoice_changes(Principal.user(ada), invoice.id)
    assert list.map(changes, change -> change.action) == ["annotate_invoice", "issue_invoice"]
    assert list.map(changes, change -> list.map(change.fields, line -> line.field)) == [
        ["note"],
        ["clerk_id", "number", "discount"]
    ]
    assert list.any(
        changes,
        change -> list.any(change.fields, line -> line.after == "Paid by card")
    )
}

test "another clerk sees each change without the discount" {
    var ada: Clerk = attempt hire_clerk("ada")
    var bea: Clerk = attempt hire_clerk("bea")
    var invoice: Invoice = issue_invoice(Principal.user(ada), "INV-2")
    var changes: List<InvoiceChange> = invoice_changes(Principal.user(bea), invoice.id)
    assert list.map(changes, change -> list.map(change.fields, line -> line.field)) == [
        ["clerk_id", "number"]
    ]
}
```

`domains/billing/routes/invoices.test.jadpo`:

```jadpo
use billing/entities { Clerk, Invoice }
use billing/actions { hire_clerk, issue_invoice }

test "the route answers with the changes, for anyone signed in" {
    var ada: Clerk = attempt hire_clerk("ada")
    var invoice: Invoice = issue_invoice(Principal.user(ada), "INV-3")
    var response: Response = request {
        method: HttpMethod.get
        path: "/invoices/{invoice_id}/changes"
        params: {
            invoice_id: invoice.id
        }
        as: Principal.user(ada)
    }
    assert response.status == 200
}
```

```sh
jadpo test
```

```text
tested: 3 passed, 2 action tests, 1 route test
```

## How long entries last

The entity's built-in purge schedule deletes each entry when its retention ends.
An entry outlives its record otherwise: deleting an invoice leaves its history
until the history's own retention ends, though `history` then gives an empty page.
A person's erasure deletes the history of every record it deletes.

The audit trail records changes to data. Who read or exported a person's data is
kept in [access records](/docs/access-records.md) instead.

To send entries somewhere else, subscribe to the built-in `AuditEntryRecorded`
event. Each entry is then also emitted in the same transaction, and your subscriber
gets it like any other event. The database stays the record of truth.

## Read more

[Audit trail](/docs/audit-trail.md),
[Subscribers](/docs/subscribers.md),
[The permissions report](/docs/the-permissions-report.md) and
[Marked fields in logs](/docs/marking-personal-fields.md#marked-fields-in-logs).

## Not built yet

An audited entity can't yet hold a value its history would have to keep in a way
Jadpo hasn't built. Each of these is `toolchain.not_built_yet` where it is written:

- An [`Email`](/docs/types/type-email.md), a field marked `personal:`, or a standard type that is personal data
  whatever holds it, such as a [`PhoneNumber`](/docs/types/type-phone-number.md).
- A `File` field, or a field with a `retention:` of its own.
- A person's record removed other than by their erasure, such as by a `delete` in
  code or by a cascade.
- A `delete` whose cascade deletes, or whose `OnDelete.cleared` changes, an audited
  record. Make the reference `OnDelete.restricted`, as `clerk_id` is here, and
  change those records first.
- An erasure that keeps an audited record, anonymised or retained.

An [`Email`](/docs/types/type-email.md) on `Invoice` gives this, its `rule:` and `help:` lines left out:

```text
error: Auditing `Invoice` isn't built yet
 --> domains/billing/entities/invoice.jadpo:26:5
   |
12 |     customer_email: Email
   |     -------------- an `Email`
26 |     audit: {
   |     ^^^^^ decided, not built
  reason: `customer_email` is an `Email`, personal data (`PersonalData.contact`) whatever holds it, and its history keeps a personal value only as a keyed hash, as logs do, which isn't built yet (area 15, "Marked fields in logs"): accepted, the value would be copied into every entry.
  next: Leave `audit:` out of `Invoice` for now (agent)
```

Sending entries outward through `publishing` isn't built yet either. It comes with
outgoing webhooks. Subscribe to `AuditEntryRecorded` for now.
