Class: AtlasRb::Resource

Inherits:
Object
  • Object
show all
Extended by:
FaradayHelper
Defined in:
lib/atlas_rb/resource.rb

Overview

Abstract base for every Atlas resource type.

Subclasses define a ROUTE constant (e.g. "/communities/") and override whichever of find / create / destroy / update / metadata / mods apply. The Resource class itself ships four endpoints that are not type-specific: a generic resolver, an XML preview helper, a permissions lookup, and an audit-event history fetch.

The Atlas resource hierarchy is:

{Community}  →  {Collection}  →  {Work}  →  {FileSet}  →  {Blob}

Subclasses extend FaradayHelper so that connection(...) and multipart(...) are available as class methods.

Direct Known Subclasses

Blob, Collection, Community, Compilation, Delegate, FileSet, Person, Work

Constant Summary

Constants included from FaradayHelper

FaradayHelper::ASSERTION_AUDIENCE, FaradayHelper::ASSERTION_ISSUER, FaradayHelper::ASSERTION_TTL, FaradayHelper::INSTRUMENTATION_EVENT

Class Method Summary collapse

Methods included from FaradayHelper

connection, multipart, system_connection, with_file_part

Class Method Details

.descendant_works(id, page: nil, per_page: nil, include_linked: nil, nuid: nil, on_behalf_of: nil) ⇒ AtlasRb::Mash?

Every Work beneath a resource, at any depth — the structural counterpart to Compilation.contents. Wraps GET /resources/<id>/descendant_works, which flattens the resource's full descendant subtree to the Works it contains, gated to what the caller may read and paginated Solr-side. Gives a Collection the flatten-to-Works capability a Set already has, so a bulk export (e.g. hyperion) pages one gated, fast call family instead of the client-side children → find_many → recurse walk.

Returns the same digest shape as find_many / Compilation.contents ({ "id", "noid", "klass", "title", "thumbnail" }) under a "works" key, plus a "pagination" envelope (total / page / per_page / pages). Membership is structural (a_member_of) only; pass include_linked: true to also surface linked members (a_linked_member_of). Restricted Works never appear for a caller who may not read them; tombstoned Works are dropped.

Examples:

Page a Collection's whole subtree of Works

result = AtlasRb::Resource.descendant_works("col-456", per_page: 100)
result["works"].map { |w| w["noid"] }
result.dig("pagination", "pages")

Parameters:

  • id (String)

    an Atlas resource ID (subtree root; any type).

  • page (Integer, nil) (defaults to: nil)

    1-based page (default 1 server-side).

  • per_page (Integer, nil) (defaults to: nil)

    page size (server default 25, capped 100).

  • include_linked (Boolean, nil) (defaults to: nil)

    also include linked members when truthy; structural-only by default.

  • nuid (String, nil) (defaults to: nil)

    optional acting user's NUID. On the relay-signing path it is signed into the assertion sub; on the BYO-JWT (ATLAS_JWT) path it is ignored (identity lives in the token).

  • on_behalf_of (String, nil) (defaults to: nil)

    optional NUID for the On-Behalf-Of header. Falls through to AtlasRb.config.default_on_behalf_of when omitted.

Returns:

  • (AtlasRb::Mash, nil)

    the parsed envelope, with a "works" digest array and a "pagination" block; nil when the id resolves to nothing (404).



125
126
127
128
129
130
131
132
133
134
135
# File 'lib/atlas_rb/resource.rb', line 125

def self.descendant_works(id, page: nil, per_page: nil, include_linked: nil, nuid: nil, on_behalf_of: nil)
  params = {}
  params[:page]           = page           if page
  params[:per_page]       = per_page       if per_page
  params[:include_linked] = include_linked unless include_linked.nil?
  resp = connection(params, nuid, on_behalf_of: on_behalf_of)
         .get('/resources/' + id + '/descendant_works')
  return nil if resp.status == 404

  AtlasRb::Mash.new(JSON.parse(resp.body))
end

.find(id, nuid: nil, on_behalf_of: nil) ⇒ Hash{String => String, Hash}?

Resolve any Atlas resource by ID without knowing its type up front.

The Atlas server returns a single-key JSON object whose key names the resource type ("community", "collection", "work", etc.); this method splits that into a normalized { "klass" => ..., "resource" => ... } pair so callers can dispatch on type.

Examples:

Polymorphic lookup

AtlasRb::Resource.find("abc123")
# => { "klass" => "Work", "resource" => { "id" => "abc123", "title" => "..." } }

Parameters:

  • id (String)

    an Atlas resource ID of any type.

  • nuid (String, nil) (defaults to: nil)

    optional acting user's NUID. On the relay-signing path it is signed into the assertion sub; on the BYO-JWT (ATLAS_JWT) path it is ignored (identity lives in the token).

  • on_behalf_of (String, nil) (defaults to: nil)

    optional NUID for the On-Behalf-Of header. Falls through to AtlasRb.config.default_on_behalf_of when omitted.

Returns:

  • (Hash{String => String, Hash}, nil)

    hash with two keys, or nil when the id resolves to nothing (404):

    • "klass" — the resource type, capitalized (e.g. "Work").
    • "resource" — the resource payload as a Hash.

Raises:

  • (AtlasRb::ResourceError)

    on any non-2xx other than 404 / 410 (e.g. an auth/validation error envelope), carrying Atlas's status + body.



45
46
47
48
49
50
51
# File 'lib/atlas_rb/resource.rb', line 45

def self.find(id, nuid: nil, on_behalf_of: nil)
  result = fetch_resource('/resources/' + id, nuid: nuid, on_behalf_of: on_behalf_of)
  return nil if result.nil?

  AtlasRb::Mash.new("klass" => result.first[0].capitalize,
                    "resource" => result.first[1])
end

.find_many(ids, nuid: nil, on_behalf_of: nil) ⇒ Array<AtlasRb::Mash>

Resolve many resources by NOID in a single round-trip.

Wraps Atlas's POST /resources/find_many, which returns one lightweight digest per resolvable resource — { "id", "noid", "klass", "title", "thumbnail", "tombstoned" } — rather than full typed payloads. Use it anywhere a set of ids would otherwise be resolved with a find-per-id fan-out (breadcrumb chains, linked-member lists, load-destination pickers): one HTTP call instead of N.

The ids travel in the request body, so the list is not bounded by URL length. The result is unordered and may be shorter than the input — unresolvable ids are dropped silently, and tombstoned resources come back flagged ("tombstoned" => true) rather than omitted. Index the result by "noid"; do not assume positional correspondence with ids.

Examples:

Resolve a set of collection titles in one call

nodes  = AtlasRb::Resource.find_many(["col-456", "col-457", "missing"])
by_noid = nodes.index_by { |n| n["noid"] }
by_noid["col-456"].title   # => "Some Collection"

Parameters:

  • ids (Array<String>)

    resource NOIDs to resolve. (Raw Valkyrie ids are not a supported input — the endpoint resolves alternate ids only.)

  • nuid (String, nil) (defaults to: nil)

    optional acting user's NUID. On the relay-signing path it is signed into the assertion sub; on the BYO-JWT (ATLAS_JWT) path it is ignored (identity lives in the token).

  • on_behalf_of (String, nil) (defaults to: nil)

    optional NUID for the On-Behalf-Of header. Falls through to AtlasRb.config.default_on_behalf_of when omitted.

Returns:

  • (Array<AtlasRb::Mash>)

    one digest Mash per resolved resource (dot- or string-keyed access); empty when nothing resolved.



84
85
86
87
88
89
# File 'lib/atlas_rb/resource.rb', line 84

def self.find_many(ids, nuid: nil, on_behalf_of: nil)
  JSON.parse(
    connection({}, nuid, on_behalf_of: on_behalf_of)
      .post('/resources/find_many', JSON.dump(ids: Array(ids)))&.body
  ).map { |node| AtlasRb::Mash.new(node) }
end

.history(id, nuid: nil, on_behalf_of: nil) ⇒ AtlasRb::Mash

TODO:

Add pagination support once Atlas's history endpoint exposes page / per_page query params. Today the endpoint returns the full history in one shot.

Fetch the audit-event history for a resource.

Wraps Atlas's GET /resources/<id>/history endpoint, which returns the full envelope (resource_id + reverse-chronological events array). The whole envelope is preserved so callers can confirm the events belong to the requested resource; access events as result["events"].

Authorization errors (401 / 403) are intentionally not caught here — they surface as raw Faraday responses for the calling application's rescue layer to translate.

Examples:

result = AtlasRb::Resource.history("abc12345")
result["resource_id"]            # => "abc12345"
result["events"].first["action"] # => "create"

Parameters:

  • id (String)

    an Atlas resource ID.

  • nuid (String, nil) (defaults to: nil)

    optional acting user's NUID. On the relay-signing path it is signed into the assertion sub; on the BYO-JWT (ATLAS_JWT) path it is ignored (identity lives in the token).

  • on_behalf_of (String, nil) (defaults to: nil)

    optional NUID for the On-Behalf-Of header. Falls through to AtlasRb.config.default_on_behalf_of when omitted.

Returns:

  • (AtlasRb::Mash)

    the parsed envelope from GET /resources/<id>/history, with "resource_id" and an "events" array (reverse chronological; possibly empty).



212
213
214
215
216
217
# File 'lib/atlas_rb/resource.rb', line 212

def self.history(id, nuid: nil, on_behalf_of: nil)
  AtlasRb::Mash.new(JSON.parse(
    connection({}, nuid, on_behalf_of: on_behalf_of)
      .get('/resources/' + id + '/history')&.body
  ))
end

.mods(id, kind = nil, nuid: nil, on_behalf_of: nil) ⇒ String?

Fetch the CURRENT MODS of any Modsable resource by NOID — the polymorphic sibling of Work.mods / Collection.mods / Community.mods. Wraps GET /resources/<id>/mods[.kind] and returns the raw response body (not parsed), mirroring the typed wrappers. Lets a caller holding only a NOID (no type) fetch descriptive MODS in one call, instead of resolving the klass first to pick the typed route — e.g. a bulk Collection/Set MODS export that has bare member NOIDs from Collection.children.

Examples:

Bulk-export a Set's members' MODS without klass dispatch

AtlasRb::Collection.children(set_id).each do |noid|
  xml = AtlasRb::Resource.mods(noid, "xml")
end

Parameters:

  • id (String)

    an Atlas resource ID (NOID).

  • kind (String, nil) (defaults to: nil)

    response-format extension: omit for the JSON projection (the server default), or pass "xml" for MODS XML ("json" / "html" also accepted). Output is byte-identical to the typed route for the resolved type.

  • nuid (String, nil) (defaults to: nil)

    optional acting user's NUID. On the relay-signing path it is signed into the assertion sub; on the BYO-JWT (ATLAS_JWT) path it is ignored (identity lives in the token).

  • on_behalf_of (String, nil) (defaults to: nil)

    optional NUID for the On-Behalf-Of header. Falls through to AtlasRb.config.default_on_behalf_of when omitted.

Returns:

  • (String, nil)

    the raw MODS body (XML or JSON per kind). The server returns 404 (empty body) for an unknown id, a non-Modsable resource, or one with no MODS.



245
246
247
248
249
# File 'lib/atlas_rb/resource.rb', line 245

def self.mods(id, kind = nil, nuid: nil, on_behalf_of: nil)
  connection({}, nuid, on_behalf_of: on_behalf_of).get(
    '/resources/' + id + '/mods' + (kind.to_s.empty? ? '' : ".#{kind}")
  )&.body
end

.mods_version(id, version_id, kind: nil, nuid: nil, on_behalf_of: nil) ⇒ String

Fetch the MODS document as of a specific version.

Wraps Atlas's GET /resources/<id>/mods/versions/<version_id> and returns the raw response body (not parsed) — mirroring Work.mods. Pass a version_id obtained from mods_versions (an opaque OCFL vN label).

Only XML is version-recoverable: the JSON access copy is overwritten in place, so the server serves historical XML (the default). kind: is accepted for parity with Work.mods but XML is currently the only supported format. An unknown version yields a 404 (raw Faraday response).

Examples:

Diff two MODS versions

old_xml = AtlasRb::Resource.mods_version("w-789", "v3")
new_xml = AtlasRb::Resource.mods_version("w-789", "v5")

Parameters:

  • id (String)

    an Atlas resource ID.

  • version_id (String)

    an OCFL version label from mods_versions, e.g. "v3".

  • kind (String, nil) (defaults to: nil)

    response format extension. Omit (or pass "xml") for the historical XML — the only format the server retains.

  • nuid (String, nil) (defaults to: nil)

    optional acting user's NUID. On the relay-signing path it is signed into the assertion sub; on the BYO-JWT (ATLAS_JWT) path it is ignored (identity lives in the token).

  • on_behalf_of (String, nil) (defaults to: nil)

    optional NUID for the On-Behalf-Of header. Falls through to AtlasRb.config.default_on_behalf_of when omitted.

Returns:

  • (String)

    the raw MODS XML body for that version.



320
321
322
323
324
325
# File 'lib/atlas_rb/resource.rb', line 320

def self.mods_version(id, version_id, kind: nil, nuid: nil, on_behalf_of: nil)
  connection({}, nuid, on_behalf_of: on_behalf_of).get(
    '/resources/' + id + '/mods/versions/' + version_id +
      (kind.to_s.empty? ? '' : ".#{kind}")
  )&.body
end

.mods_versions(id, nuid: nil, on_behalf_of: nil) ⇒ AtlasRb::Mash

List the retained MODS versions for a resource.

Wraps Atlas's GET /resources/<id>/mods/versions, which returns the full envelope — resource_id plus a reverse-chronological versions array — as an AtlasRb::Mash. Each version descriptor mirrors the audit-event shape (version_id, created, actor_nuid, on_behalf_of_nuid, source, note) so the two streams render with the same helpers; actor fields are correlated from the audit log and may be null when a version has no matching edit event.

Type-agnostic: pass any Modsable resource ID (Community, Collection, Work). A resource with no MODS comes back as { "versions" => [] }.

Version labels are opaque, sortable OCFL vN strings — not a 1-based counter — so treat them as identifiers to feed back into mods_version, not as ordinals. The server admin-gates this endpoint (it exposes edit attribution); 401 / 403 surface as raw Faraday responses, matching history.

Examples:

history = AtlasRb::Resource.mods_versions("w-789")
history["versions"].first["version_id"] # => "v5"
history["versions"].first["actor_nuid"]  # => "000000002"

Parameters:

  • id (String)

    an Atlas resource ID.

  • nuid (String, nil) (defaults to: nil)

    optional acting user's NUID. On the relay-signing path it is signed into the assertion sub; on the BYO-JWT (ATLAS_JWT) path it is ignored (identity lives in the token).

  • on_behalf_of (String, nil) (defaults to: nil)

    optional NUID for the On-Behalf-Of header. Falls through to AtlasRb.config.default_on_behalf_of when omitted.

Returns:

  • (AtlasRb::Mash)

    the parsed envelope, with "resource_id" and a "versions" array (reverse chronological; possibly empty).



284
285
286
287
288
289
# File 'lib/atlas_rb/resource.rb', line 284

def self.mods_versions(id, nuid: nil, on_behalf_of: nil)
  AtlasRb::Mash.new(JSON.parse(
    connection({}, nuid, on_behalf_of: on_behalf_of)
      .get('/resources/' + id + '/mods/versions')&.body
  ))
end

.permissions(id, nuid: nil, on_behalf_of: nil) ⇒ Hash

Fetch the access-control entries for a resource.

Examples:

AtlasRb::Resource.permissions("abc123")
# => { "id" => "abc123", "read" => [...], "write" => [...] }

Parameters:

  • id (String)

    an Atlas resource ID.

  • nuid (String, nil) (defaults to: nil)

    optional acting user's NUID. On the relay-signing path it is signed into the assertion sub; on the BYO-JWT (ATLAS_JWT) path it is ignored (identity lives in the token).

  • on_behalf_of (String, nil) (defaults to: nil)

    optional NUID for the On-Behalf-Of header. Falls through to AtlasRb.config.default_on_behalf_of when omitted.

Returns:

  • (Hash)

    the "resource" payload from GET /resources/<id>/permissions, typically containing read/write/admin grant lists.



175
176
177
178
179
180
# File 'lib/atlas_rb/resource.rb', line 175

def self.permissions(id, nuid: nil, on_behalf_of: nil)
  AtlasRb::Mash.new(JSON.parse(
    connection({}, nuid, on_behalf_of: on_behalf_of)
      .get('/resources/' + id + '/permissions')&.body
  ))["resource"]
end

.preview(xml_path, nuid: nil, on_behalf_of: nil) ⇒ String

Validate a MODS XML document against Atlas's schema without persisting it.

Useful for surfacing validation errors in UIs before the user commits.

Examples:

AtlasRb::Resource.preview("/tmp/draft-mods.xml")

Parameters:

  • xml_path (String)

    path to a MODS XML file on disk.

  • nuid (String, nil) (defaults to: nil)

    optional acting user's NUID. On the relay-signing path it is signed into the assertion sub; on the BYO-JWT (ATLAS_JWT) path it is ignored (identity lives in the token).

  • on_behalf_of (String, nil) (defaults to: nil)

    optional NUID for the On-Behalf-Of header. Falls through to AtlasRb.config.default_on_behalf_of when omitted.

Returns:

  • (String)

    the raw response body from POST /resources/preview — typically a JSON or XML error report.



153
154
155
156
157
158
# File 'lib/atlas_rb/resource.rb', line 153

def self.preview(xml_path, nuid: nil, on_behalf_of: nil)
  payload = { binary: Faraday::Multipart::FilePart.new(File.open(xml_path),
                                                       "application/xml",
                                                       File.basename(xml_path)) }
  multipart(nuid, on_behalf_of: on_behalf_of).post('/resources/preview', payload)&.body
end