All history is stored in one table, resource_version, created by sql/install.sql. There are no foreign keys into Omeka's core tables, so history survives the deletion of the resource, the user, or anything the snapshot refers to.
Table columns
| Column | Type | Description |
|---|---|---|
id |
INT UNSIGNED | Version row id. This is the id used in admin URLs. |
resource_type |
VARCHAR(20) | items, media, or item_sets |
resource_id |
INT UNSIGNED | Omeka resource id |
version_num |
INT UNSIGNED | Sequential per resource, starting at 1 |
operation |
VARCHAR(20) | create, update, delete, restore, or baseline |
payload |
MEDIUMTEXT | Full JSON-LD of the resource, as returned by its API representation |
payload_hash |
CHAR(64) | SHA-256 of the payload with volatile keys removed, used for deduplication |
resource_title |
VARCHAR(255) | Display title at capture time (o:title, then dcterms:title, then o:source, else [untitled]) |
user_id, user_name |
INT, VARCHAR(255) | The acting user's id and name (or email), or NULL for system saves |
is_batch |
TINYINT(1) | 1 when the save was a partial update (Bulk Edit, batchUpdate, REST PATCH) |
batch_key |
VARCHAR(64) | 28-character hex key shared by every capture in the same PHP request or job |
restored_from |
INT UNSIGNED | For restore operations, the version row id that was restored |
created_at |
DATETIME | Capture time in the server's PHP time zone |
Indexes cover (resource_type, resource_id, version_num), (user_id, created_at), created_at, and batch_key.
Payloads are stored as MEDIUMTEXT, so a single snapshot can be up to 16 MB. Payloads are not compressed.
Operations
| Operation | When it is written |
|---|---|
create |
After api.create.post for a versioned resource type |
update |
After api.update.post, unless the payload hash equals the latest version's |
delete |
After api.delete.post; always written, even when the payload matches |
restore |
An update performed by the module's restore service, single or batch |
baseline |
Before the first update of a resource that has no versions yet, capturing its pre-edit state |
Deduplication
Before inserting, the module hashes the payload with the keys @context, @id, o:created, and o:modified removed and all object keys sorted recursively (list order is kept, because value order inside a property is meaningful). If the hash matches the latest version of that resource, and the operation is not delete, nothing is written.
Capture hooks
The module listens on the shared event identifier Omeka\Api\Adapter\AbstractAdapter for api.create.post, api.update.post, api.delete.post (capture) and api.update.pre (baseline), all at priority -100 so that other listeners run first. Any exception in a capture handler is written to the PHP error log with the prefix [ResourceVersioning] and swallowed; a capture failure never causes the save to fail.
Saves that bypass the API manager are not captured. In particular, media edited inline within an item's edit form are hydrated directly by the item adapter, so they produce no media version; the item's own version still records the list of attached media.
Admin URLs
All routes are under /admin/resource-versioning and are handled by ResourceVersioning\Controller\Admin\VersionController.
| URL | Method | Action |
|---|---|---|
/admin/resource-versioning |
GET | Browse all versions; query parameters resource_type, user_id, operation, date_from, date_to, page |
/admin/resource-versioning/resource/:resource-type/:id |
GET | History for one resource (items, media, or item_sets) |
/admin/resource-versioning/version/:id |
GET | One version with its raw JSON-LD snapshot |
/admin/resource-versioning/compare/:a[/:b] |
GET | Diff two version rows; b defaults to the version before a |
/admin/resource-versioning/restore/:id |
GET, POST | Confirmation page, then execute the restore |
/admin/resource-versioning/batch/:key |
GET | All versions written under one batch key |
/admin/resource-versioning/rollback |
GET, POST | Batch rollback wizard; POST with step=preview or step=execute |
/admin/resource-versioning/prune |
POST | Dispatch the prune job |
POST actions require the CSRF token from the module's confirmation form, which the pages include automatically.