Skip to content

Zotero integration

BraDypUS can link any record in any data table to one or more items in online Zotero libraries (user or group). Citations are cached locally and refreshed on demand; Zotero remains the authoritative source.


Architecture overview

bdus_zotero_libs          bdus_zotero_links
─────────────────         ──────────────────────────────────
id                        id
type   (user | group)     tb         (table name)
zotero_id                 record_id
name                      lib_id  ──► bdus_zotero_libs.id
api_key  (nullable)       zotero_key
citation_style            pages, notes, sort
created_at                author_year, full_citation  ← cache
                          zotero_version              ← cache
                          synced_at                   ← cache
                          detached (default 0)
                          created_at

bdus_zotero_links.lib_id has a CASCADE FK to bdus_zotero_libs.id: deleting a library removes all its links automatically.


DB migration

Both tables are created by M023 (DB\System\Migrations\M023_ZoteroTables). The migration runs automatically on the first request after upgrade (idempotent).


PHP layer

Zotero\Client

lib/Zotero/Client.php — thin wrapper around the Zotero REST API v3.

MethodDescription
search(string $q, int $limit): arrayFull-text search; returns raw item arrays
getItems(array $keys): arrayBatch fetch up to 50 keys; returns keyed by zotero_key
getFormattedCitation(string $key, string $style): stringHTML citation via CSL API
extractAuthorYear(array $item): stringBest-effort Author YYYY string from item data
buildPublicUrl(string $key): ?stringGroup URL zotero.org/groups/{id}/items/{key}; null for user libs

Constructor: new Client(string $type, string $zoteroId, ?string $apiKey).

Default citation style: chicago-author-date (Client::DEFAULT_STYLE).

Bdus\Controllers\Zotero

controllers/Zotero.php — controller with inline privilege guards.

RoutePrivilegeDescription
GET /api/zotero/libsadminList libraries (api_key redacted)
POST /api/zotero/libadminAdd a library
DELETE /api/zotero/lib/{id}adminRemove a library
GET /api/zotero/search?lib_id&q&limiteditProxy search to Zotero
GET /api/zotero/links/{tb}/{id}readGet links for a record
POST /api/zotero/linkeditAdd a link (fetches and caches citation)
PATCH /api/zotero/link/{id}editUpdate pages / notes / sort
DELETE /api/zotero/link/{id}editRemove a link
POST /api/zotero/sync/{tb}/{id}editSync cache for one record
POST /api/zotero/syncadminSync all referenced items

API keys are never returned by getLibs(); the response includes has_api_key: bool instead.

Record\Read::getBibliography()

Called by getFull() — adds a bibliography key to the record model. Joins bdus_zotero_links with bdus_zotero_libs and builds zotero_url for group libraries. Guards against missing M023 tables to stay safe on pre-upgrade instances.


Sync strategy

Per-record (automatic): ZoteroSection.vue calls POST /api/zotero/sync/{tb}/{id} in onMounted, after the record is already displayed. If the Zotero version of any linked item has changed, the cache columns are updated and the component refreshes the list. Non-fatal — network errors are swallowed.

Global (admin): The Sync all button in Config → Zotero Libraries calls POST /api/zotero/sync, which processes one representative row per unique (lib_id, zotero_key) pair.

Detached items: If a Zotero item no longer exists in the library, detached is set to 1. The UI shows an orange warning badge; the link is never deleted automatically.


Frontend

ZoteroSection.vue

src/components/record/ZoteroSection.vue — record detail section.

  • Props: bibliography (Object), editMode (Boolean), recordTb, recordId
  • Emits: bib-added, bib-deleted
  • Features: background sync on mount, drag-to-sort (SortableJS), inline pages/notes editing (@blur save), external Zotero link for group libs, detached badge, search-and-add panel with library Select + debounced input.

ZoteroLibsPanel.vue

src/components/config/ZoteroLibsPanel.vue — admin config panel (reached via Config → Zotero Libraries in the sidebar).

  • List all libraries with type badge, Zotero ID, citation style, API key status.
  • Form to add a new library (type, ID, name, optional API key, optional citation style — defaults to chicago-author-date).
  • Delete button per library.
  • Sync all button with result message.

Integration in RecordView.vue

ZoteroSection is rendered after ManualLinksSection and before the Geodata section:

vue
<ZoteroSection
  v-if="hasBibliography || mode === 'edit'"
  :bibliography="record.bibliography ?? {}"
  :editMode="mode === 'edit'"
  :recordTb="record.metadata.tb_id"
  :recordId="id"
  @bib-added="onBibAdded"
  @bib-deleted="onBibDeleted"
/>

hasBibliography is a computed that checks Object.keys(record.bibliography ?? {}).length > 0.


Tests

PHPUnit:

  • tests/Integration/M023MigrationTest.php — 11 tests covering table creation, column set, indexes, uniqueness, default values, idempotency.
  • tests/Integration/ZoteroCtrlTest.php — 40 tests covering all controller methods (privilege guards, parameter validation, CRUD, error paths). Network-dependent calls (search with real API, sync with items) are exercised only for their parameter-validation and error-handling paths.

Hurl E2E:

  • tests/api/17_zotero.hurl — 16 requests covering the full HTTP API lifecycle: list → add lib → verify → add link → edit link → verify → sync → delete link → delete lib → verify.

Adding a new citation style

Citation styles follow the CSL style repository identifiers (e.g. apa, mla, harvard-cite-them-right). Set citation_style when adding a library or leave it empty to use chicago-author-date.