Batch execution with zot

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", and "trash". Patch and Trash rows identify an existing item; create rows contain a new-item payload.

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> Correct one reviewed title 
#>   digest: 34f12497d5349ed6e1397c32f5bedfa29625bb9006ad73c5258cb4aa38f0a9b3 
#>    action library     N
#>    <char>  <char> <int>
#> 1:  patch    user     1

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.

Approval <- zotApprove(Preview, notes = "Offline documentation example")
identical(Approval$digest, Preview$digest)
#> [1] TRUE

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. Callers should not modify preview$plan after creating the approval because the current executor does not recalculate its digest.

Execute with a ledger

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

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$ABCD1234$status
#> [1] "done"
Store$title
#> [1] "A reviewed 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". Create rows are recorded as done after the new item can be read; Trash rows use the status returned by zotTrash().

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

LedgerAgain <- zotExecute(
  Config,
  preview = Preview,
  approval = Approval,
  ledgerPath = LedgerPath
)
LedgerAgain$items$ABCD1234$status
#> [1] "done"

Sample completed patches

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

Verification <- zotVerify(Config, preview = Preview, ledger = LedgerAgain)
Verification
#> $counts
#> Statuses
#> done 
#>    1 
#> 
#> $sampleChecked
#> [1] 1
#> 
#> $sampleConfirmed
#> [1] 1
unlink(LedgerPath)

Applications should inspect failed or missing ledger entries separately. Local SQLite and semantic-index results are useful for discovery but do not replace an API read of remote state.