atlas_rb 1.3.3 → 1.3.4
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 +4 -4
- data/.version +1 -1
- data/CHANGELOG.md +46 -0
- data/Gemfile.lock +2 -2
- data/lib/atlas_rb/compilation.rb +374 -0
- data/lib/atlas_rb/errors.rb +41 -7
- data/lib/atlas_rb/middleware/raise_on_resource_error.rb +19 -9
- data/lib/atlas_rb.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f1d65e6631474391c3971b939e6394049b6b5f4198a14ffd55bd5e9c51d2bbf
|
|
4
|
+
data.tar.gz: d3371babc6a6aca8141a3692b399fd9989d5f8f29341ac376e3d1bed5850d0f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f9450d1ed3d4c77708b619c4c48686c35606cee0d8633d9fcd448440127398b613f2d664fb672bbfaccb0bf44143ccbf8731aac29e3d967f09b56177ab7517ae
|
|
7
|
+
data.tar.gz: acb806ac1e60026594bac52eb124415439daa77ceb219cc5465dc961d16ab78b2dc481fe64256a5cbf109e67347170ac15e2f484fb7d19ea248b69406a950834
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.3.
|
|
1
|
+
1.3.4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.4
|
|
4
|
+
|
|
5
|
+
### Added — Compilation (DRS "Sets") bindings
|
|
6
|
+
|
|
7
|
+
Bindings for Atlas's Compilation surface (Atlas v0.6.57) — personal,
|
|
8
|
+
curated, recipe-based groupings of Works and Collections, the persistence
|
|
9
|
+
behind the Cerberus Sets UI.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
set = AtlasRb::Compilation.create("Course readings", nuid: "000000002")
|
|
13
|
+
|
|
14
|
+
# Recipe lines — each mutation returns the full updated compilation
|
|
15
|
+
AtlasRb::Compilation.add_included_collection(set["id"], "col-456", nuid: "000000002")
|
|
16
|
+
AtlasRb::Compilation.add_included_work(set["id"], "w-789", nuid: "000000002")
|
|
17
|
+
AtlasRb::Compilation.add_exclusion(set["id"], "w-790", nuid: "000000002") # set aside
|
|
18
|
+
AtlasRb::Compilation.remove_exclusion(set["id"], "w-790", nuid: "000000002") # put back
|
|
19
|
+
|
|
20
|
+
# Make public, resolve the recipe
|
|
21
|
+
AtlasRb::Compilation.update(set["id"],
|
|
22
|
+
permissions: { read: ["public"], edit: [], edit_users: [] },
|
|
23
|
+
nuid: "000000002")
|
|
24
|
+
AtlasRb::Compilation.contents(set["id"]).contents.map(&:noid)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `Compilation.create / find / update / destroy / list` — owner-scoped
|
|
28
|
+
CRUD. The depositor is stamped server-side from the acting NUID and is
|
|
29
|
+
immutable; `list(owner:)` (cross-owner) is admin-only. `update` takes
|
|
30
|
+
`title:` / `description:` / `permissions:` (the ACL hash replaces all
|
|
31
|
+
three grant lists; ACL changes are audited server-side, no-ops
|
|
32
|
+
suppressed).
|
|
33
|
+
- Six membership calls (`add/remove_included_collection`,
|
|
34
|
+
`add/remove_included_work`, `add/remove_exclusion`) — each returns the
|
|
35
|
+
updated `"compilation"` object so chip counts refresh without a
|
|
36
|
+
follow-up `find`. Adds and removes are idempotent; the type rules
|
|
37
|
+
(Works and Collections only, no Communities) are enforced by Atlas.
|
|
38
|
+
- `Compilation.contents` wraps `GET /compilations/<id>/contents` — the
|
|
39
|
+
recipe resolved to `find_many`-style digests with Solr-side pagination
|
|
40
|
+
(`{ total, page, per_page, pages }`). Included for completeness; CERES
|
|
41
|
+
hits the endpoint directly and Cerberus resolves contents via its own
|
|
42
|
+
Blacklight query.
|
|
43
|
+
- New `AtlasRb::CompilationError` (422 — blank title, wrong-type or
|
|
44
|
+
unknown membership noid), the Compilation sibling of
|
|
45
|
+
`LinkedMemberError`. `AtlasRb::ForbiddenError` now also covers 403s on
|
|
46
|
+
the Compilation surface, so a non-grantee reading a private Set gets a
|
|
47
|
+
typed refusal instead of a swallowed `nil`.
|
|
48
|
+
|
|
3
49
|
## 1.3.3
|
|
4
50
|
|
|
5
51
|
### Added — multipage bindings (FileSet ordinality)
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
atlas_rb (1.3.
|
|
4
|
+
atlas_rb (1.3.4)
|
|
5
5
|
faraday (~> 2.7)
|
|
6
6
|
faraday-follow_redirects (~> 0.3.0)
|
|
7
7
|
faraday-multipart (~> 1)
|
|
@@ -23,7 +23,7 @@ GEM
|
|
|
23
23
|
net-http (~> 0.5)
|
|
24
24
|
hashie (5.1.0)
|
|
25
25
|
logger
|
|
26
|
-
json (2.19.
|
|
26
|
+
json (2.19.9)
|
|
27
27
|
logger (1.7.0)
|
|
28
28
|
multipart-post (2.4.1)
|
|
29
29
|
net-http (0.9.1)
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtlasRb
|
|
4
|
+
# A Compilation (DRS "Set") — a personal, curated, recipe-based grouping
|
|
5
|
+
# of {Work}s and {Collection}s.
|
|
6
|
+
#
|
|
7
|
+
# The recipe is three noid lists: included collections (resolved
|
|
8
|
+
# transitively — the collection plus everything beneath it), individually
|
|
9
|
+
# included works, and excluded works ("set-asides", subtracted from the
|
|
10
|
+
# resolved union). Atlas resolves the recipe at read time via
|
|
11
|
+
# {.contents}; nothing is materialized.
|
|
12
|
+
#
|
|
13
|
+
# Compilations are Atlas-side ActiveRecord (ephemeral curation, not
|
|
14
|
+
# repository content), but carry a minted NOID as their public id — so
|
|
15
|
+
# ids here look exactly like every other resource's. There is no `/mods`,
|
|
16
|
+
# thumbnail, or tombstone surface to bind. Membership rules (Works and
|
|
17
|
+
# Collections only, no Communities) are enforced server-side; a rejected
|
|
18
|
+
# add surfaces as {AtlasRb::CompilationError} (422), an authorization
|
|
19
|
+
# refusal as {AtlasRb::ForbiddenError} (403).
|
|
20
|
+
#
|
|
21
|
+
# See also: {Work.add_linked_member} — the membership add/remove pairs
|
|
22
|
+
# here mirror that precedent.
|
|
23
|
+
class Compilation < Resource
|
|
24
|
+
# Atlas REST endpoint prefix for this resource.
|
|
25
|
+
# @api private
|
|
26
|
+
ROUTE = "/compilations/"
|
|
27
|
+
|
|
28
|
+
# Fetch a single Compilation by ID.
|
|
29
|
+
#
|
|
30
|
+
# Visibility is per-row: the owner, holders of an explicit read/edit
|
|
31
|
+
# grant, and (for public Sets) anyone — a private Set read by a
|
|
32
|
+
# non-grantee raises {AtlasRb::ForbiddenError}.
|
|
33
|
+
#
|
|
34
|
+
# @param id [String] the Compilation ID (NOID).
|
|
35
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
36
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
37
|
+
# tokens still resolve without it.
|
|
38
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
39
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
40
|
+
# omitted.
|
|
41
|
+
# @return [Hash] the `"compilation"` object, already unwrapped — `id`,
|
|
42
|
+
# `title`, `description`, `depositor`, the three recipe arrays
|
|
43
|
+
# (`included_collections`, `included_works`, `excluded_works`), the
|
|
44
|
+
# ACL arrays, and timestamps.
|
|
45
|
+
# @raise [AtlasRb::ForbiddenError] if the caller may not read this Set.
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# AtlasRb::Compilation.find("c-123", nuid: "000000002")
|
|
49
|
+
# # => { "id" => "c-123", "title" => "Course readings", ... }
|
|
50
|
+
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
51
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
52
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
|
|
53
|
+
))["compilation"]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# List Compilations, owner-scoped and paginated (newest first).
|
|
57
|
+
#
|
|
58
|
+
# Defaults to the acting user's own Sets. Pass `owner:` to list another
|
|
59
|
+
# user's — that is admin-only and raises {AtlasRb::ForbiddenError} for
|
|
60
|
+
# anyone else. There is no public browse surface.
|
|
61
|
+
#
|
|
62
|
+
# @param owner [String, nil] NUID whose Sets to list (admin-only when it
|
|
63
|
+
# isn't the acting user). Omit for "my Sets".
|
|
64
|
+
# @param page [Integer, nil] 1-indexed page number.
|
|
65
|
+
# @param per_page [Integer, nil] page size override.
|
|
66
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
67
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
68
|
+
# tokens still resolve without it.
|
|
69
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
70
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
71
|
+
# omitted.
|
|
72
|
+
# @return [AtlasRb::Mash] `{ "compilations" => [...], "pagination" => {...} }`.
|
|
73
|
+
# Each entry wraps the same `"compilation"` object {.find} returns.
|
|
74
|
+
# @raise [AtlasRb::ForbiddenError] on a cross-owner listing without admin.
|
|
75
|
+
#
|
|
76
|
+
# @example My Sets
|
|
77
|
+
# AtlasRb::Compilation.list(nuid: "000000002")
|
|
78
|
+
#
|
|
79
|
+
# @example Another user's Sets (admin)
|
|
80
|
+
# AtlasRb::Compilation.list(owner: "000000002", nuid: "000000004")
|
|
81
|
+
def self.list(owner: nil, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil)
|
|
82
|
+
params = {}
|
|
83
|
+
params[:owner] = owner if owner
|
|
84
|
+
params[:page] = page if page
|
|
85
|
+
params[:per_page] = per_page if per_page
|
|
86
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
87
|
+
connection(params, nuid, on_behalf_of: on_behalf_of).get(ROUTE)&.body
|
|
88
|
+
))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Create a Compilation owned by the acting user.
|
|
92
|
+
#
|
|
93
|
+
# The depositor (owner) is stamped server-side from the authenticated
|
|
94
|
+
# NUID — it is not a parameter and is immutable post-create. New Sets
|
|
95
|
+
# are born private: empty ACLs, no staff default.
|
|
96
|
+
#
|
|
97
|
+
# @param title [String] the Set's title (required; blank is a 422).
|
|
98
|
+
# @param description [String, nil] optional free-text description.
|
|
99
|
+
# @param nuid [String, nil] the acting user's NUID, forwarded as the
|
|
100
|
+
# `User:` header — the created Set's owner. Required for
|
|
101
|
+
# cerberus-token requests.
|
|
102
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
103
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
104
|
+
# omitted.
|
|
105
|
+
# @return [Hash] the created `"compilation"` object, already unwrapped.
|
|
106
|
+
# @raise [AtlasRb::CompilationError] if Atlas rejects the create (422 —
|
|
107
|
+
# e.g. a blank title).
|
|
108
|
+
# @raise [AtlasRb::ForbiddenError] if the caller may not create Sets
|
|
109
|
+
# (guests cannot).
|
|
110
|
+
#
|
|
111
|
+
# @example
|
|
112
|
+
# AtlasRb::Compilation.create("Course readings",
|
|
113
|
+
# description: "HIST 1101",
|
|
114
|
+
# nuid: "000000002")
|
|
115
|
+
def self.create(title, description: nil, nuid: nil, on_behalf_of: nil)
|
|
116
|
+
params = { title: title }
|
|
117
|
+
params[:description] = description if description
|
|
118
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
119
|
+
connection(params, nuid, on_behalf_of: on_behalf_of).post(ROUTE)&.body
|
|
120
|
+
))["compilation"]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Update a Compilation's title / description / ACL.
|
|
124
|
+
#
|
|
125
|
+
# Only the keys you pass are written. The `permissions:` hash replaces
|
|
126
|
+
# all three grant lists at once (`read:` / `edit:` group lists plus
|
|
127
|
+
# `edit_users:` NUIDs); the depositor is never writable. Server-side,
|
|
128
|
+
# an ACL change emits a `permissions` audit event (no-op ACL writes are
|
|
129
|
+
# suppressed); recipe membership has its own calls and emits nothing.
|
|
130
|
+
#
|
|
131
|
+
# @param id [String] the Compilation ID.
|
|
132
|
+
# @param title [String, nil] new title.
|
|
133
|
+
# @param description [String, nil] new description.
|
|
134
|
+
# @param permissions [Hash, nil] ACL replacement, e.g.
|
|
135
|
+
# `{ read: ["public"], edit: [], edit_users: ["000000003"] }`.
|
|
136
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
137
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
138
|
+
# tokens still resolve without it.
|
|
139
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
140
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
141
|
+
# omitted.
|
|
142
|
+
# @return [Hash] the updated `"compilation"` object, already unwrapped.
|
|
143
|
+
# @raise [AtlasRb::CompilationError] if Atlas rejects the update (422).
|
|
144
|
+
# @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights
|
|
145
|
+
# (owner / explicit grant / admin).
|
|
146
|
+
#
|
|
147
|
+
# @example Rename
|
|
148
|
+
# AtlasRb::Compilation.update("c-123", title: "Renamed", nuid: "000000002")
|
|
149
|
+
#
|
|
150
|
+
# @example Make public (the CERES case)
|
|
151
|
+
# AtlasRb::Compilation.update("c-123",
|
|
152
|
+
# permissions: { read: ["public"], edit: [], edit_users: [] },
|
|
153
|
+
# nuid: "000000002")
|
|
154
|
+
def self.update(id, title: nil, description: nil, permissions: nil, nuid: nil, on_behalf_of: nil)
|
|
155
|
+
params = {}
|
|
156
|
+
params[:title] = title if title
|
|
157
|
+
params[:description] = description if description
|
|
158
|
+
params[:permissions] = permissions if permissions
|
|
159
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
160
|
+
connection(params, nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id)&.body
|
|
161
|
+
))["compilation"]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Destroy a Compilation.
|
|
165
|
+
#
|
|
166
|
+
# Owner (or edit-grantee / admin) only. The recipe rows go with it; the
|
|
167
|
+
# Works and Collections it referenced are untouched — a Set is a view,
|
|
168
|
+
# not a container.
|
|
169
|
+
#
|
|
170
|
+
# @param id [String] the Compilation ID.
|
|
171
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
172
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
173
|
+
# tokens still resolve without it.
|
|
174
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
175
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
176
|
+
# omitted.
|
|
177
|
+
# @return [Faraday::Response] the raw response. Status `204` on success.
|
|
178
|
+
# @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
|
|
179
|
+
#
|
|
180
|
+
# @example
|
|
181
|
+
# AtlasRb::Compilation.destroy("c-123", nuid: "000000002")
|
|
182
|
+
def self.destroy(id, nuid: nil, on_behalf_of: nil)
|
|
183
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).delete(ROUTE + id)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Add an include-collection recipe line: everything under the
|
|
187
|
+
# Collection (transitively) joins the Set's resolved contents.
|
|
188
|
+
#
|
|
189
|
+
# Idempotent — re-adding an included collection is a no-op. The noid
|
|
190
|
+
# must resolve to a Collection: Communities and unknown ids are
|
|
191
|
+
# rejected server-side as a 422.
|
|
192
|
+
#
|
|
193
|
+
# @param id [String] the Compilation ID.
|
|
194
|
+
# @param collection_id [String] the Collection NOID to include.
|
|
195
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
196
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
197
|
+
# tokens still resolve without it.
|
|
198
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
199
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
200
|
+
# omitted.
|
|
201
|
+
# @return [Hash] the updated `"compilation"` object — the response is
|
|
202
|
+
# the full recipe, so chip counts refresh without a follow-up {.find}.
|
|
203
|
+
# @raise [AtlasRb::CompilationError] if the noid is not a Collection (422).
|
|
204
|
+
# @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
|
|
205
|
+
#
|
|
206
|
+
# @example
|
|
207
|
+
# AtlasRb::Compilation.add_included_collection("c-123", "col-456", nuid: "000000002")
|
|
208
|
+
def self.add_included_collection(id, collection_id, nuid: nil, on_behalf_of: nil)
|
|
209
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
210
|
+
connection({ collection_id: collection_id }, nuid, on_behalf_of: on_behalf_of)
|
|
211
|
+
.post(ROUTE + id + '/included_collections')&.body
|
|
212
|
+
))["compilation"]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Remove an include-collection recipe line.
|
|
216
|
+
#
|
|
217
|
+
# Idempotent — removing a collection that is not in the recipe is a
|
|
218
|
+
# 200 no-op (nothing for a client to recover from).
|
|
219
|
+
#
|
|
220
|
+
# @param id [String] the Compilation ID.
|
|
221
|
+
# @param collection_id [String] the Collection NOID to remove.
|
|
222
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
223
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
224
|
+
# tokens still resolve without it.
|
|
225
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
226
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
227
|
+
# omitted.
|
|
228
|
+
# @return [Hash] the updated `"compilation"` object.
|
|
229
|
+
# @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
|
|
230
|
+
#
|
|
231
|
+
# @example
|
|
232
|
+
# AtlasRb::Compilation.remove_included_collection("c-123", "col-456", nuid: "000000002")
|
|
233
|
+
def self.remove_included_collection(id, collection_id, nuid: nil, on_behalf_of: nil)
|
|
234
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
235
|
+
connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
236
|
+
.delete(ROUTE + id + '/included_collections/' + collection_id)&.body
|
|
237
|
+
))["compilation"]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Add an include-work recipe line: one Work, included individually.
|
|
241
|
+
#
|
|
242
|
+
# Idempotent. The noid must resolve to a Work; anything else is a 422.
|
|
243
|
+
#
|
|
244
|
+
# @param id [String] the Compilation ID.
|
|
245
|
+
# @param work_id [String] the Work NOID to include.
|
|
246
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
247
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
248
|
+
# tokens still resolve without it.
|
|
249
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
250
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
251
|
+
# omitted.
|
|
252
|
+
# @return [Hash] the updated `"compilation"` object.
|
|
253
|
+
# @raise [AtlasRb::CompilationError] if the noid is not a Work (422).
|
|
254
|
+
# @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
|
|
255
|
+
#
|
|
256
|
+
# @example
|
|
257
|
+
# AtlasRb::Compilation.add_included_work("c-123", "w-789", nuid: "000000002")
|
|
258
|
+
def self.add_included_work(id, work_id, nuid: nil, on_behalf_of: nil)
|
|
259
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
260
|
+
connection({ work_id: work_id }, nuid, on_behalf_of: on_behalf_of)
|
|
261
|
+
.post(ROUTE + id + '/included_works')&.body
|
|
262
|
+
))["compilation"]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Remove an include-work recipe line. Idempotent.
|
|
266
|
+
#
|
|
267
|
+
# @param id [String] the Compilation ID.
|
|
268
|
+
# @param work_id [String] the Work NOID to remove.
|
|
269
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
270
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
271
|
+
# tokens still resolve without it.
|
|
272
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
273
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
274
|
+
# omitted.
|
|
275
|
+
# @return [Hash] the updated `"compilation"` object.
|
|
276
|
+
# @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
|
|
277
|
+
#
|
|
278
|
+
# @example
|
|
279
|
+
# AtlasRb::Compilation.remove_included_work("c-123", "w-789", nuid: "000000002")
|
|
280
|
+
def self.remove_included_work(id, work_id, nuid: nil, on_behalf_of: nil)
|
|
281
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
282
|
+
connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
283
|
+
.delete(ROUTE + id + '/included_works/' + work_id)&.body
|
|
284
|
+
))["compilation"]
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Set a Work aside: subtract it from the Set's resolved contents even
|
|
288
|
+
# though an included collection covers it.
|
|
289
|
+
#
|
|
290
|
+
# Idempotent. The noid must resolve to a Work. Setting aside a Work
|
|
291
|
+
# that no inclusion currently covers is legal — the recipe lines are
|
|
292
|
+
# independent; the subtraction just matches nothing.
|
|
293
|
+
#
|
|
294
|
+
# @param id [String] the Compilation ID.
|
|
295
|
+
# @param work_id [String] the Work NOID to set aside.
|
|
296
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
297
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
298
|
+
# tokens still resolve without it.
|
|
299
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
300
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
301
|
+
# omitted.
|
|
302
|
+
# @return [Hash] the updated `"compilation"` object.
|
|
303
|
+
# @raise [AtlasRb::CompilationError] if the noid is not a Work (422).
|
|
304
|
+
# @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
|
|
305
|
+
#
|
|
306
|
+
# @example
|
|
307
|
+
# AtlasRb::Compilation.add_exclusion("c-123", "w-789", nuid: "000000002")
|
|
308
|
+
def self.add_exclusion(id, work_id, nuid: nil, on_behalf_of: nil)
|
|
309
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
310
|
+
connection({ work_id: work_id }, nuid, on_behalf_of: on_behalf_of)
|
|
311
|
+
.post(ROUTE + id + '/exclusions')&.body
|
|
312
|
+
))["compilation"]
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Put a set-aside Work back. Idempotent.
|
|
316
|
+
#
|
|
317
|
+
# @param id [String] the Compilation ID.
|
|
318
|
+
# @param work_id [String] the Work NOID to restore to the resolved set.
|
|
319
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
320
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
321
|
+
# tokens still resolve without it.
|
|
322
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
323
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
324
|
+
# omitted.
|
|
325
|
+
# @return [Hash] the updated `"compilation"` object.
|
|
326
|
+
# @raise [AtlasRb::ForbiddenError] if the caller lacks edit rights.
|
|
327
|
+
#
|
|
328
|
+
# @example
|
|
329
|
+
# AtlasRb::Compilation.remove_exclusion("c-123", "w-789", nuid: "000000002")
|
|
330
|
+
def self.remove_exclusion(id, work_id, nuid: nil, on_behalf_of: nil)
|
|
331
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
332
|
+
connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
333
|
+
.delete(ROUTE + id + '/exclusions/' + work_id)&.body
|
|
334
|
+
))["compilation"]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Resolve a Compilation's recipe into the Works it currently denotes.
|
|
338
|
+
#
|
|
339
|
+
# Wraps `GET /compilations/<id>/contents` — included for completeness;
|
|
340
|
+
# the endpoint's primary consumer is CERES (which calls Atlas directly),
|
|
341
|
+
# and Cerberus resolves Set contents via its own Blacklight query.
|
|
342
|
+
# Results are gated to what the caller may discover (public + the
|
|
343
|
+
# caller's groups; admins see everything; tombstoned works excluded) —
|
|
344
|
+
# the same semantics as Cerberus gated discovery.
|
|
345
|
+
#
|
|
346
|
+
# @param id [String] the Compilation ID.
|
|
347
|
+
# @param page [Integer, nil] 1-indexed page number (default 1).
|
|
348
|
+
# @param per_page [Integer, nil] page size (default 25, capped at 100).
|
|
349
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
350
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
351
|
+
# tokens still resolve without it.
|
|
352
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
353
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
354
|
+
# omitted.
|
|
355
|
+
# @return [AtlasRb::Mash] `{ "contents" => [...], "pagination" =>
|
|
356
|
+
# { "total", "page", "per_page", "pages" } }`. Each entry is a
|
|
357
|
+
# lightweight digest in the {Resource.find_many} vocabulary —
|
|
358
|
+
# `id` / `noid` / `klass` / `title` / `thumbnail`.
|
|
359
|
+
# @raise [AtlasRb::ForbiddenError] if the caller may not read this Set.
|
|
360
|
+
#
|
|
361
|
+
# @example
|
|
362
|
+
# page = AtlasRb::Compilation.contents("c-123", nuid: "000000002")
|
|
363
|
+
# page.contents.map(&:noid)
|
|
364
|
+
# page.pagination.total
|
|
365
|
+
def self.contents(id, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil)
|
|
366
|
+
params = {}
|
|
367
|
+
params[:page] = page if page
|
|
368
|
+
params[:per_page] = per_page if per_page
|
|
369
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
370
|
+
connection(params, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/contents')&.body
|
|
371
|
+
))
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
data/lib/atlas_rb/errors.rb
CHANGED
|
@@ -101,14 +101,48 @@ module AtlasRb
|
|
|
101
101
|
end
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
# Raised when Atlas
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
104
|
+
# Raised when Atlas rejects a Compilation (Set) write with a `422`
|
|
105
|
+
# carrying a machine-readable `error` discriminator — a blank title on
|
|
106
|
+
# create/update (`invalid_record`), or a membership add whose noid does
|
|
107
|
+
# not resolve to the expected type (a Community where a Collection is
|
|
108
|
+
# required, an unknown id, a Collection where a Work is required).
|
|
108
109
|
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
110
|
+
# The Compilation sibling of {LinkedMemberError}; same shape, same
|
|
111
|
+
# rationale (the binding's `["compilation"]` unwrap would otherwise
|
|
112
|
+
# discard the envelope on a non-2xx).
|
|
113
|
+
#
|
|
114
|
+
# rescue AtlasRb::CompilationError => e
|
|
115
|
+
# flash.now[:alert] = e.message
|
|
116
|
+
#
|
|
117
|
+
# @note Authorization failures surface as {ForbiddenError} (HTTP 403).
|
|
118
|
+
class CompilationError < Error
|
|
119
|
+
# @return [String, nil] the machine-readable error code from the
|
|
120
|
+
# envelope (currently `"invalid_record"`).
|
|
121
|
+
attr_reader :code
|
|
122
|
+
|
|
123
|
+
# @return [String, nil] the rejected resource's ID, from the envelope
|
|
124
|
+
# (may be nil — validation envelopes don't always carry one).
|
|
125
|
+
attr_reader :resource_id
|
|
126
|
+
|
|
127
|
+
# @param message [String] human-readable rejection description.
|
|
128
|
+
# @param code [String, nil] the envelope's `error` discriminator.
|
|
129
|
+
# @param resource_id [String, nil] the rejected resource's ID.
|
|
130
|
+
def initialize(message, code: nil, resource_id: nil)
|
|
131
|
+
super(message)
|
|
132
|
+
@code = code
|
|
133
|
+
@resource_id = resource_id
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Raised when Atlas refuses a re-parent, linked-member, or Compilation
|
|
138
|
+
# request with an HTTP `403`, whose envelope is
|
|
139
|
+
# `{ "error", "action", "subject" }`. Lets callers distinguish "you may
|
|
140
|
+
# not do this" from a structural rejection ({ReparentError} /
|
|
141
|
+
# {LinkedMemberError} / {CompilationError}) or a not-found.
|
|
142
|
+
#
|
|
143
|
+
# @note Scoped to the re-parent / linked-member write paths and the
|
|
144
|
+
# Compilation surface — `403`s on other endpoints still surface as raw
|
|
145
|
+
# responses for the caller's own rescue layer, unchanged.
|
|
112
146
|
class ForbiddenError < Error
|
|
113
147
|
# @return [String, nil] the envelope's `error` value.
|
|
114
148
|
attr_reader :code
|
|
@@ -15,30 +15,34 @@ module AtlasRb
|
|
|
15
15
|
# {RaiseOnStaleResource}.
|
|
16
16
|
#
|
|
17
17
|
# It is intentionally narrow — it only fires on the re-parent
|
|
18
|
-
# (`.../parent`) and linked-member (`.../linked_members...`) write paths
|
|
19
|
-
# and
|
|
18
|
+
# (`.../parent`) and linked-member (`.../linked_members...`) write paths
|
|
19
|
+
# and the Compilation surface (`/compilations...`), and only on
|
|
20
|
+
# `403` / `422` bodies carrying an `error` discriminator.
|
|
20
21
|
# Everything else (other paths, other statuses, a `422` whose body uses a
|
|
21
22
|
# different discriminator such as `tombstone`'s `code: "has_live_children"`)
|
|
22
23
|
# passes through untouched, so atlas_rb stays a thin Faraday binding that
|
|
23
24
|
# translates only the wire signals callers genuinely need to discriminate.
|
|
24
25
|
#
|
|
25
26
|
# Mapping:
|
|
26
|
-
# - `403` (
|
|
27
|
+
# - `403` (any covered path) → {AtlasRb::ForbiddenError} (`error`/`action`/`subject`)
|
|
27
28
|
# - `422` on `.../parent` → {AtlasRb::ReparentError} (`error`/`resource_id`)
|
|
28
29
|
# - `422` on `.../linked_members...` → {AtlasRb::LinkedMemberError}
|
|
30
|
+
# - `422` on `/compilations...` → {AtlasRb::CompilationError}
|
|
29
31
|
class RaiseOnResourceError < Faraday::Middleware
|
|
30
32
|
# @param env [Faraday::Env] the completed response environment.
|
|
31
|
-
# @raise [AtlasRb::ForbiddenError] on a 403 to a
|
|
33
|
+
# @raise [AtlasRb::ForbiddenError] on a 403 to a covered path.
|
|
32
34
|
# @raise [AtlasRb::ReparentError] on a 422 to a re-parent path.
|
|
33
35
|
# @raise [AtlasRb::LinkedMemberError] on a 422 to a linked-member path.
|
|
36
|
+
# @raise [AtlasRb::CompilationError] on a 422 to a Compilation path.
|
|
34
37
|
# @return [void]
|
|
35
38
|
def on_complete(env)
|
|
36
39
|
return unless env.status == 403 || env.status == 422
|
|
37
40
|
|
|
38
|
-
path
|
|
39
|
-
reparent
|
|
40
|
-
linked
|
|
41
|
-
|
|
41
|
+
path = env.url&.path.to_s
|
|
42
|
+
reparent = path.end_with?("/parent")
|
|
43
|
+
linked = path.include?("/linked_members")
|
|
44
|
+
compilation = path.start_with?("/compilations")
|
|
45
|
+
return unless reparent || linked || compilation
|
|
42
46
|
|
|
43
47
|
body = parse_json(env.body)
|
|
44
48
|
return unless body.is_a?(Hash) && body["error"]
|
|
@@ -56,12 +60,18 @@ module AtlasRb
|
|
|
56
60
|
code: body["error"],
|
|
57
61
|
resource_id: body["resource_id"]
|
|
58
62
|
)
|
|
59
|
-
|
|
63
|
+
elsif linked
|
|
60
64
|
raise AtlasRb::LinkedMemberError.new(
|
|
61
65
|
body["message"] || "Atlas rejected the linked-member write",
|
|
62
66
|
code: body["error"],
|
|
63
67
|
resource_id: body["resource_id"]
|
|
64
68
|
)
|
|
69
|
+
else
|
|
70
|
+
raise AtlasRb::CompilationError.new(
|
|
71
|
+
body["message"] || "Atlas rejected the compilation write",
|
|
72
|
+
code: body["error"],
|
|
73
|
+
resource_id: body["resource_id"]
|
|
74
|
+
)
|
|
65
75
|
end
|
|
66
76
|
end
|
|
67
77
|
|
data/lib/atlas_rb.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative "atlas_rb/work"
|
|
|
18
18
|
require_relative "atlas_rb/file_set"
|
|
19
19
|
require_relative "atlas_rb/blob"
|
|
20
20
|
require_relative "atlas_rb/delegate"
|
|
21
|
+
require_relative "atlas_rb/compilation"
|
|
21
22
|
require_relative "atlas_rb/user"
|
|
22
23
|
require_relative "atlas_rb/admin"
|
|
23
24
|
require_relative "atlas_rb/admin/work"
|
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.3.
|
|
4
|
+
version: 1.3.4
|
|
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-
|
|
11
|
+
date: 2026-06-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -124,6 +124,7 @@ files:
|
|
|
124
124
|
- lib/atlas_rb/blob.rb
|
|
125
125
|
- lib/atlas_rb/collection.rb
|
|
126
126
|
- lib/atlas_rb/community.rb
|
|
127
|
+
- lib/atlas_rb/compilation.rb
|
|
127
128
|
- lib/atlas_rb/configuration.rb
|
|
128
129
|
- lib/atlas_rb/delegate.rb
|
|
129
130
|
- lib/atlas_rb/errors.rb
|