---
title: "Batch execution with zot"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Batch execution with zot}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

`zot` provides a preview, approval-record, execution, ledger, and verification
sequence for batch plans. This vignette runs the sequence against an in-memory
request performer. It makes no network requests and changes no Zotero library.

## Create a plan

A plan is a `data.table` with one action per row. Supported actions are
`"patch"`, `"create"`, `"attach"`, and `"trash"`. Patch and Trash rows
identify an existing item; create rows contain a new-item payload; attach rows
describe a local file to upload as a new attachment.

```{r}
library(data.table)
library(zot)

Plan <- data.table(
  action = "patch",
  library = "user",
  itemKey = "ABCD1234",
  payload = list(list(title = "A reviewed title"))
)

Preview <- zotPreview(Plan, describe = "Correct one reviewed title")
Preview
```

`zotPreview()` validates the required columns and action values, copies the
rows, and records a SHA-256 digest. It does not make an API request.

## Create an approval record

`zotApprove()` stores the preview digest, optional notes, and a timestamp.

```{r}
Approval <- zotApprove(Preview, notes = "Offline documentation example")
identical(Approval$digest, Preview$digest)
```

The function records application metadata; it does not authenticate a person
or determine whether execution is authorized. The executor requires the
approval and preview digests to match, then recalculates the digest from
`preview$plan`. Changing the plan after approval is refused before any request;
the changed plan requires its own preview and approval.

## Execute with a ledger

The performer below models a versioned Zotero item. It supports the GET and
PATCH calls used by the example.

```{r}
Store <- new.env(parent = emptyenv())
Store$version <- 1L
Store$title <- "Original title"

Performer <- function(config, method, path, query, body, version) {
  if (method == "GET") {
    return(list(
      status = 200L,
      headers = list(),
      body = list(
        version = Store$version,
        data = list(title = Store$title)
      )
    ))
  }

  if (method == "PATCH" && identical(as.integer(version), Store$version)) {
    Store$title <- body$title
    Store$version <- Store$version + 1L
    return(list(status = 204L, headers = list(), body = NULL))
  }

  list(status = 412L, headers = list(), body = NULL)
}

Config <- zotConfig(
  userID = "42",
  key = "example-key",
  performer = Performer
)

LedgerPath <- tempfile(fileext = ".json")
Ledger <- zotExecute(
  Config,
  preview = Preview,
  approval = Approval,
  ledgerPath = LedgerPath
)

Ledger$items[["user:ABCD1234"]]$status
Store$title
```

For a patch row, `zotExecute()` reads the current item version, sends a
conditional patch, reads the item again, and compares the requested fields.
A matching result is recorded as `"done"`. A create first saves its returned
key as `"created"`, then reads that key and confirms the payload before marking
it done. A failed read retains the key and error; resuming only retries the
read, never the POST. A transport exception during POST leaves `"attempt"`
because the effect is unknown and requires reconciliation. Trash rows use the
status returned by `zotTrash()`.

An attach row, usually built by `ztAttachPlan()`, carries the file's local
`path`, its `md5` and `filesize`, the stored `filename`, `contentType`, an
optional `charset`, and `parentItem` (`NULL` for a standalone attachment). The
preview digest covers those fields, so the approval binds the exact bytes, and
`counts` reports the bytes each library will receive. Execution moves the row
through these states:

| State | Meaning |
| --- | --- |
| `"attempt"` | Recorded before the attachment item is posted; if it remains, the POST outcome is unknown and resuming stops for reconciliation. |
| `"failed"` | Nothing was created: the file's MD5 differs from the approved one, refused before any request, or Zotero rejected the item. Resuming retries the row. |
| `"created"` | The attachment item exists and its key is saved; the upload is pending, failed, or not yet confirmed, with the `error` recorded. Resuming checks the file again and repeats the upload on that key, never posting a second item. |
| `"done"` | The upload was authorized (an identical stored file counts), sent and registered, and a read of the item confirmed its MD5, filename, content type and parent. |

The upload uses the configuration's `fileTransport`, which tests and examples
replace as they replace the request performer.

New ledgers have `schema = 2` and identify existing items by `library:key`;
creates retain `create-<row>` for block moves. Legacy ledgers without a schema
keep their key-only format when every id maps to exactly one plan row. A
legacy ledger with the same key in two libraries is refused without being
rewritten. A legacy failed create also needs reconciliation, because earlier
executors could lose its key after the POST succeeded.

Reusing the ledger path for the same digest skips entries already marked done:

```{r}
LedgerAgain <- zotExecute(
  Config,
  preview = Preview,
  approval = Approval,
  ledgerPath = LedgerPath
)
LedgerAgain$items[["user:ABCD1234"]]$status
```

## Sample completed patches

`zotVerify()` summarizes ledger statuses and re-reads at most ten completed
patch rows. It does not sample create, attach or Trash rows.

```{r}
Verification <- zotVerify(Config, preview = Preview, ledger = LedgerAgain)
Verification
unlink(LedgerPath)
```

Missing plan rows are counted as `"pending"` in `counts` and listed in
`unfinished`. `complete` is false for unfinished rows or a failed read-back;
a successful sample still does not prove every completed row was re-read.
Use `full = TRUE` to re-read all completed rows, attach rows included. Applications should inspect
failed entries separately.
Local SQLite results are useful for discovery but do not
replace an API read of remote state.
