Skip to contents

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.

library(data.table)
#> 
#> Attaching package: 'data.table'
#> The following object is masked from 'package:base':
#> 
#>     %notin%
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 bytes
#>    <char>  <char> <int> <num>
#> 1:  patch    user     1     0

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

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
#> [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". 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:

LedgerAgain <- zotExecute(
  Config,
  preview = Preview,
  approval = Approval,
  ledgerPath = LedgerPath
)
LedgerAgain$items[["user: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, attach or Trash rows.

Verification <- zotVerify(Config, preview = Preview, ledger = LedgerAgain)
Verification
#> $counts
#> Statuses
#> done 
#>    1 
#> 
#> $sampleChecked
#> [1] 1
#> 
#> $sampleConfirmed
#> [1] 1
#> 
#> $unconfirmed
#> character(0)
#> 
#> $unfinished
#> character(0)
#> 
#> $complete
#> [1] TRUE
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.