--- 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-` 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.