External integrations
external-integrations.Rmdzot has two boundaries beyond ordinary in-memory
planning: the local Zotero SQLite database and Zotero file storage.
Their dependencies and effects are intentionally different.
library(data.table)
#>
#> Attaching package: 'data.table'
#> The following object is masked from 'package:base':
#>
#> %notin%
library(zot)| Boundary | Reads or effects | Operational note |
|---|---|---|
| Local SQLite | Read-only local query | May lag synchronized API state |
| File attachment | Creates an item and may upload bytes | Has immediate API and storage effects |
Query SQLite read-only
zotLocalQuery() opens the supplied database with
SQLite’s read-only flag and closes the connection when the query
returns. This small fixture demonstrates the boundary without depending
on an installed Zotero client.
Path <- tempfile(fileext = ".sqlite")
Connection <- DBI::dbConnect(RSQLite::SQLite(), Path)
DBI::dbWriteTable(
Connection,
"items",
data.frame(key = c("A", "B"), title = c("One", "Two"))
)
DBI::dbDisconnect(Connection)
zotLocalQuery(
"SELECT key, title FROM items WHERE key = ?",
params = list("B"),
path = Path
)
#> key title
#> <char> <char>
#> 1: B Two
unlink(Path)zotLocalItems() knows Zotero’s client schema and returns
non-trashed regular items with library and date fields:
Items <- zotLocalItems(library = "user")
Items[, .(key, itemType, title, dateModified)]The desktop client may be in WAL mode or awaiting synchronization. These rows are useful for a scan, not for proving a remote write succeeded.
Exercise the attachment protocol offline
ztAttach() creates an attachment item, requests storage
authorization, uploads bytes when needed, registers the upload, and
verifies the server-side MD5. The example supplies a fake API performer
and a transport that reports an identical file already present, so no
bytes leave the process.
File <- tempfile(fileext = ".pdf")
writeBin(as.raw(1:8), File)
Performer <- function(config, method, path, query, body, version) {
list(
status = 200L,
headers = list(),
body = list(successful = list(`0` = list(key = "ATT1")))
)
}
Config <- zotConfig(
userID = "42",
key = "example-key",
performer = Performer
)
Transport <- list(
authorize = function(...) list(exists = 1L),
upload = function(...) stop("upload should not run"),
register = function(...) stop("registration should not run")
)
ztAttach(
Config,
parentKey = NA_character_,
filePath = File,
fileTransport = Transport
)
#> $status
#> [1] "exists"
#>
#> $key
#> [1] "ATT1"
unlink(File)With the default transport, this call has immediate external effects and may consume Zotero storage quota. A file-bearing record is refused by the block-move planner because that planner does not transfer attachment bytes.
Without an explicit contentType, the type is chosen as
Zotero’s desktop client chooses it: a table of known extensions first
(md and markdown as
text/markdown, bib and txt as
text/plain, pdf, epub,
html, csv, json, office formats
and images by their registered types), then, for any other extension, a
sniff of the first 1024 bytes that yields text/plain when
every byte is a text character and application/octet-stream
otherwise. Text files are therefore never stored as
application/octet-stream, which keeps them readable and
indexable in Zotero. A text/* file whose bytes are valid
UTF-8 is also given charset = "utf-8", and an empty file is
refused.
ztAttach() acts at once. To review the files before
anything is created, build an attach plan with
ztAttachPlan() and run it through preview, approval,
execution and verification as in
vignette("guarded-workflow"); the command line exposes the
same route as zot plan attach.
Keep evidence in the correct plane
A curation workflow may discover candidates through SQLite, establish current versions through the API, and upload an attachment. The same functions can be invoked interactively, from scripts, or by agent applications. API reads are required to observe remote outcomes; local data sources can lag the synchronized library.