atlas_rb 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29da2e41c70d17572c6c24002ba0ff25beb2ad6cdd940dee157a83133c86efaf
4
- data.tar.gz: 9e16fed8e22867c37647a12de104135d78f57995ba5e6decd4929ac6a402bae7
3
+ metadata.gz: '0947add262eb2b9b4ab993f16c2c379877455e9ece3bca04f41a61bfddf79ee3'
4
+ data.tar.gz: d239887dfaac21e78a7118503d4ed8ab79b12b3aa174ee18f5a8f31f253ca866
5
5
  SHA512:
6
- metadata.gz: 7ba5a0216a5cc5dacce4f6822078eb07d71aa4e14b31142c83dd823c0ec177efa92e1712eac0b235c125870611dcd4633817066c5ee70a98d6f85adbec614a7f
7
- data.tar.gz: b176fc377d6d802e356c5f0a0fd487c6c899d4b5e323e3017eb773fd11e06dd8ebb415329a4c4e81aef5f55f0086602fc33a6cb82c4699709e0a3209c542a48d
6
+ metadata.gz: b4d59334c7730d11f9f7779b1cbef5c7cef49ce4b1726257a9c8590f258ab4c8aac80ffffa10c1111a38b4d98b95c7b36e824eab925aff4aae1ff87336f7e37e
7
+ data.tar.gz: a5c0484e6d4204758dc1eee8e2ca6086a289e19e250a404fbdffc4af65eaa7864f2da7f93a9586d4d84776c76b33dbdebb5147ae4d728baed51110864c347e30
data/.version CHANGED
@@ -1 +1 @@
1
- 1.2.2
1
+ 1.3.1
data/CHANGELOG.md CHANGED
@@ -1,6 +1,67 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.3.1
4
+
5
+ ### Added — `AtlasRb::Resource.mods_versions` / `mods_version` (MODS version history)
6
+
7
+ Two bindings for Atlas's MODS version-history endpoints. `mods_versions`
8
+ lists the retained versions of a resource's descriptive metadata;
9
+ `mods_version` fetches the raw MODS XML as of a specific version — together
10
+ enough to drive a line-diff between any two MODS states.
11
+
12
+ ```ruby
13
+ history = AtlasRb::Resource.mods_versions("w-789")
14
+ history["versions"].first["version_id"] # => "v5" (newest)
15
+ history["versions"].first["actor_nuid"] # => "000000002"
16
+
17
+ old_xml = AtlasRb::Resource.mods_version("w-789", "v3")
18
+ new_xml = AtlasRb::Resource.mods_version("w-789", "v5")
19
+ ```
20
+
21
+ - `mods_versions` returns the full envelope (`resource_id` + a
22
+ reverse-chronological `versions` array) as an `AtlasRb::Mash`. Each
23
+ descriptor mirrors the audit-event shape (`version_id`, `created`,
24
+ `actor_nuid`, `on_behalf_of_nuid`, `source`, `note`); actor fields are
25
+ correlated from the audit log and may be `null`. Admin-gated server-side.
26
+ - `mods_version` returns the **raw XML body** (mirroring `Work.mods`). Only
27
+ XML is version-recoverable — the JSON access copy is overwritten in place
28
+ — so `kind:` is accepted for parity but XML is the only retained format.
29
+ - Version labels are opaque, sortable OCFL `vN` strings (a Blob's
30
+ preservation envelope occupies earlier versions, so the first MODS
31
+ version is typically `v3`). Treat them as identifiers to feed back into
32
+ `mods_version`, not as ordinals.
33
+ - Both are type-agnostic (Community / Collection / Work) and live on
34
+ `Resource` beside `history` / `permissions`. A resource with no MODS
35
+ returns `{ "versions" => [] }`.
36
+
37
+ ## 1.3.0
38
+
39
+ ### Added — `AtlasRb::Resource.find_many` (batch resolve by NOID)
40
+
41
+ A binding for Atlas's `POST /resources/find_many`. Resolves a set of NOIDs
42
+ to lightweight digests in **one** round-trip, replacing the `find`-per-id
43
+ fan-out that several Cerberus surfaces (breadcrumbs, linked-member lists,
44
+ load-destination pickers) paid on every render.
45
+
46
+ ```ruby
47
+ nodes = AtlasRb::Resource.find_many(["col-456", "col-457", "missing"])
48
+ by_noid = nodes.index_by { |n| n["noid"] }
49
+ by_noid["col-456"].title # => "Some Collection"
50
+ ```
51
+
52
+ - Each digest is `{ "id", "noid", "klass", "title", "thumbnail",
53
+ "tombstoned" }` — not the full typed payload. `title` / `thumbnail` are
54
+ `null` for resources off the Modsable backbone (FileSet/Blob).
55
+ - The ids ride in the request **body**, so the list isn't bounded by URL
56
+ length. Returns one `AtlasRb::Mash` per resolved resource.
57
+ - The result is **unordered** and **may be shorter than the input**:
58
+ unresolvable ids are dropped, tombstoned ones come back flagged
59
+ (`"tombstoned" => true`). Index by `"noid"` — don't assume positional
60
+ correspondence with the input.
61
+ - Resolves NOIDs (alternate ids) only; raw Valkyrie ids are not a supported
62
+ input.
63
+
64
+ ## 1.2.2
4
65
 
5
66
  ### Added — `AtlasRb::AuditEvent.emit` (session-scoped audit events)
6
67
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (1.2.2)
4
+ atlas_rb (1.3.1)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
data/README.md CHANGED
@@ -245,6 +245,44 @@ mode-less session events. Atlas stamps `occurred_at` server-side.
245
245
  Authorization errors (`401` / `403`) surface as raw Faraday responses,
246
246
  matching `Resource.history`.
247
247
 
248
+ ### MODS version history
249
+
250
+ Every descriptive-metadata edit retains the prior MODS XML on the server.
251
+ `Resource.mods_versions` lists the retained versions; `Resource.mods_version`
252
+ fetches the raw MODS XML as of one of them — together enough to render a
253
+ line-diff between any two MODS states. Both are type-agnostic (pass any
254
+ Community, Collection, or Work ID).
255
+
256
+ ```ruby
257
+ history = AtlasRb::Resource.mods_versions("w-789")
258
+ history["resource_id"] # => "w-789"
259
+ history["versions"].first["version_id"] # => "v5" (newest first)
260
+ history["versions"].first["actor_nuid"] # => "000000002" (or nil)
261
+
262
+ # Fetch two versions and diff them:
263
+ old_xml = AtlasRb::Resource.mods_version("w-789", "v3")
264
+ new_xml = AtlasRb::Resource.mods_version("w-789", "v5")
265
+ ```
266
+
267
+ `mods_versions` returns the full envelope (`resource_id` + a
268
+ reverse-chronological `versions` array) as an `AtlasRb::Mash`; each
269
+ descriptor mirrors the audit-event shape (`version_id`, `created`,
270
+ `actor_nuid`, `on_behalf_of_nuid`, `source`, `note`), so the two streams
271
+ render with the same helpers. Actor attribution is correlated from the
272
+ audit log and may be `null`. The endpoint is admin-gated server-side
273
+ (it exposes edit attribution).
274
+
275
+ `mods_version` returns the **raw XML body** — like `Work.mods`, not a Mash.
276
+ Only XML is version-recoverable (the JSON access copy is overwritten in
277
+ place), so the server serves historical XML; `kind:` is accepted for parity
278
+ with `Work.mods` but XML is the only retained format.
279
+
280
+ Version labels are **opaque, sortable OCFL `vN` strings**, not a 1-based
281
+ counter — a Blob's preservation envelope occupies earlier versions, so the
282
+ first MODS version is typically `v3`. Treat them as identifiers to feed back
283
+ into `mods_version`. A resource with no MODS returns `{ "versions" => [] }`;
284
+ `401` / `403` surface as raw Faraday responses, matching `Resource.history`.
285
+
248
286
  ### Re-parenting
249
287
 
250
288
  `reparent` moves a resource to a new structural parent, binding Atlas's
@@ -312,6 +350,27 @@ The two mutations raise the same way `reparent` does — `LinkedMemberError`
312
350
  on a structural `422` (carrying the envelope's `error` code as `#code`) and
313
351
  `ForbiddenError` on a `403` — instead of swallowing the envelope.
314
352
 
353
+ ### Batch resolve (`Resource.find_many`)
354
+
355
+ When you have a *set* of NOIDs and only need each one's title / klass /
356
+ thumbnail — breadcrumb chains, linked-member lists, load-destination
357
+ pickers — resolve them in one round-trip instead of a `find`-per-id
358
+ fan-out:
359
+
360
+ ```ruby
361
+ nodes = AtlasRb::Resource.find_many(["col-456", "col-457", "missing"])
362
+ by_noid = nodes.index_by { |n| n["noid"] }
363
+ by_noid["col-456"].title # => "Some Collection"
364
+ ```
365
+
366
+ Each entry is a lightweight digest — `{ "id", "noid", "klass", "title",
367
+ "thumbnail", "tombstoned" }` — not the full typed payload. The ids travel
368
+ in the request body (no URL-length ceiling). The result is **unordered**
369
+ and **may be shorter than the input**: unresolvable ids are dropped and
370
+ tombstoned resources come back flagged (`"tombstoned" => true`), so index
371
+ by `"noid"` rather than assuming positional correspondence. NOIDs only —
372
+ raw Valkyrie ids are not a supported input.
373
+
315
374
  ## End-to-end example
316
375
 
317
376
  JSON responses come back as `AtlasRb::Mash` (a `Hashie::Mash` subclass), so
@@ -47,6 +47,44 @@ module AtlasRb
47
47
  "resource" => result.first[1])
48
48
  end
49
49
 
50
+ # Resolve many resources by NOID in a single round-trip.
51
+ #
52
+ # Wraps Atlas's `POST /resources/find_many`, which returns one lightweight
53
+ # digest per resolvable resource — `{ "id", "noid", "klass", "title",
54
+ # "thumbnail", "tombstoned" }` — rather than full typed payloads. Use it
55
+ # anywhere a set of ids would otherwise be resolved with a `find`-per-id
56
+ # fan-out (breadcrumb chains, linked-member lists, load-destination
57
+ # pickers): one HTTP call instead of N.
58
+ #
59
+ # The ids travel in the request **body**, so the list is not bounded by
60
+ # URL length. The result is **unordered** and **may be shorter than the
61
+ # input** — unresolvable ids are dropped silently, and tombstoned
62
+ # resources come back flagged (`"tombstoned" => true`) rather than
63
+ # omitted. Index the result by `"noid"`; do not assume positional
64
+ # correspondence with `ids`.
65
+ #
66
+ # @param ids [Array<String>] resource NOIDs to resolve. (Raw Valkyrie ids
67
+ # are not a supported input — the endpoint resolves alternate ids only.)
68
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
69
+ # `User:` header. Required for cerberus-token requests; legacy bearer
70
+ # tokens still resolve without it.
71
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
72
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
73
+ # omitted.
74
+ # @return [Array<AtlasRb::Mash>] one digest Mash per resolved resource
75
+ # (dot- or string-keyed access); empty when nothing resolved.
76
+ #
77
+ # @example Resolve a set of collection titles in one call
78
+ # nodes = AtlasRb::Resource.find_many(["col-456", "col-457", "missing"])
79
+ # by_noid = nodes.index_by { |n| n["noid"] }
80
+ # by_noid["col-456"].title # => "Some Collection"
81
+ def self.find_many(ids, nuid: nil, on_behalf_of: nil)
82
+ JSON.parse(
83
+ connection({}, nuid, on_behalf_of: on_behalf_of)
84
+ .post('/resources/find_many', JSON.dump(ids: Array(ids)))&.body
85
+ ).map { |node| AtlasRb::Mash.new(node) }
86
+ end
87
+
50
88
  # Validate a MODS XML document against Atlas's schema *without* persisting it.
51
89
  #
52
90
  # Useful for surfacing validation errors in UIs before the user commits.
@@ -128,5 +166,81 @@ module AtlasRb
128
166
  .get('/resources/' + id + '/history')&.body
129
167
  ))
130
168
  end
169
+
170
+ # List the retained MODS versions for a resource.
171
+ #
172
+ # Wraps Atlas's `GET /resources/<id>/mods/versions`, which returns the
173
+ # full envelope — `resource_id` plus a reverse-chronological `versions`
174
+ # array — as an `AtlasRb::Mash`. Each version descriptor mirrors the
175
+ # audit-event shape (`version_id`, `created`, `actor_nuid`,
176
+ # `on_behalf_of_nuid`, `source`, `note`) so the two streams render with
177
+ # the same helpers; actor fields are correlated from the audit log and
178
+ # may be `null` when a version has no matching edit event.
179
+ #
180
+ # Type-agnostic: pass any Modsable resource ID (Community, Collection,
181
+ # Work). A resource with no MODS comes back as `{ "versions" => [] }`.
182
+ #
183
+ # Version labels are opaque, sortable OCFL `vN` strings — not a 1-based
184
+ # counter — so treat them as identifiers to feed back into
185
+ # {mods_version}, not as ordinals. The server admin-gates this endpoint
186
+ # (it exposes edit attribution); `401` / `403` surface as raw Faraday
187
+ # responses, matching {history}.
188
+ #
189
+ # @param id [String] an Atlas resource ID.
190
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
191
+ # `User:` header. Required for cerberus-token requests; legacy bearer
192
+ # tokens still resolve without it.
193
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
194
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
195
+ # omitted.
196
+ # @return [AtlasRb::Mash] the parsed envelope, with `"resource_id"` and a
197
+ # `"versions"` array (reverse chronological; possibly empty).
198
+ #
199
+ # @example
200
+ # history = AtlasRb::Resource.mods_versions("w-789")
201
+ # history["versions"].first["version_id"] # => "v5"
202
+ # history["versions"].first["actor_nuid"] # => "000000002"
203
+ def self.mods_versions(id, nuid: nil, on_behalf_of: nil)
204
+ AtlasRb::Mash.new(JSON.parse(
205
+ connection({}, nuid, on_behalf_of: on_behalf_of)
206
+ .get('/resources/' + id + '/mods/versions')&.body
207
+ ))
208
+ end
209
+
210
+ # Fetch the MODS document as of a specific version.
211
+ #
212
+ # Wraps Atlas's `GET /resources/<id>/mods/versions/<version_id>` and
213
+ # returns the **raw response body** (not parsed) — mirroring
214
+ # {Work.mods}. Pass a `version_id` obtained from {mods_versions} (an
215
+ # opaque OCFL `vN` label).
216
+ #
217
+ # Only XML is version-recoverable: the JSON access copy is overwritten in
218
+ # place, so the server serves historical XML (the default). `kind:` is
219
+ # accepted for parity with {Work.mods} but XML is currently the only
220
+ # supported format. An unknown version yields a `404` (raw Faraday
221
+ # response).
222
+ #
223
+ # @param id [String] an Atlas resource ID.
224
+ # @param version_id [String] an OCFL version label from {mods_versions},
225
+ # e.g. `"v3"`.
226
+ # @param kind [String, nil] response format extension. Omit (or pass
227
+ # `"xml"`) for the historical XML — the only format the server retains.
228
+ # @param nuid [String, nil] optional acting user's NUID, forwarded as the
229
+ # `User:` header. Required for cerberus-token requests; legacy bearer
230
+ # tokens still resolve without it.
231
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
232
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
233
+ # omitted.
234
+ # @return [String] the raw MODS XML body for that version.
235
+ #
236
+ # @example Diff two MODS versions
237
+ # old_xml = AtlasRb::Resource.mods_version("w-789", "v3")
238
+ # new_xml = AtlasRb::Resource.mods_version("w-789", "v5")
239
+ def self.mods_version(id, version_id, kind: nil, nuid: nil, on_behalf_of: nil)
240
+ connection({}, nuid, on_behalf_of: on_behalf_of).get(
241
+ '/resources/' + id + '/mods/versions/' + version_id +
242
+ (kind.present? ? ".#{kind}" : '')
243
+ )&.body
244
+ end
131
245
  end
132
246
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atlas_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cliff
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-03 00:00:00.000000000 Z
11
+ date: 2026-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday