atlas_rb 1.1.1 → 1.2.0
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 +43 -0
- data/Gemfile.lock +2 -2
- data/README.md +41 -0
- data/lib/atlas_rb/collection.rb +36 -0
- data/lib/atlas_rb/community.rb +42 -0
- data/lib/atlas_rb/errors.rb +39 -0
- data/lib/atlas_rb/faraday_helper.rb +2 -0
- data/lib/atlas_rb/middleware/raise_on_stale_resource.rb +50 -0
- data/lib/atlas_rb/work.rb +142 -0
- data/lib/atlas_rb.rb +4 -4
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 93e0688ad6097a3b6a36ed26db8c19d3416cb3c59beb3d95dd3cff61c6cdd176
|
|
4
|
+
data.tar.gz: 1af0ff155d288412bd6ab8a95fc50da55cfccd02d605b5bae1cf28cebdf33973
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44c8582605f238861c424a769fd4e788e08189edd969f3974d113a938f54ecce522e143ec0418cd813de958c1cbb6d9cd44b4796b6b9c190629af49a1d0417e9
|
|
7
|
+
data.tar.gz: 8ad34a6b44d4472ba300f8ef7b6c9f30d5d8674f0f69f7711e094d202ba07f2b428607b463ce8261e21e2fa5bbcbca865da9de3fb3af8e28d6e8906810037a12
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.2.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Added — Tree/DAG foundation bindings
|
|
6
|
+
|
|
7
|
+
Thin Faraday mirrors for the two membership mutations Atlas shipped as
|
|
8
|
+
part of the DRS "Tree/DAG foundation" (re-parenting and linked members).
|
|
9
|
+
No client-side logic — the gem mirrors Atlas's wire and never queries
|
|
10
|
+
Solr.
|
|
11
|
+
|
|
12
|
+
- **`AtlasRb::Collection.reparent(id, new_parent_id, nuid: nil, on_behalf_of: nil)`**
|
|
13
|
+
- **`AtlasRb::Community.reparent(id, new_parent_id, nuid: nil, on_behalf_of: nil)`**
|
|
14
|
+
- **`AtlasRb::Work.reparent(id, new_collection_id, nuid: nil, on_behalf_of: nil)`**
|
|
15
|
+
|
|
16
|
+
Bind `PATCH /<type>/:id/parent` with a `{ parent_id }` body, moving a
|
|
17
|
+
resource to a new structural parent. Mirrors `create`'s single-parent-id
|
|
18
|
+
shape and returns the updated resource (same shape as `find`), reflecting
|
|
19
|
+
the new `a_member_of`. `Community.reparent` accepts `new_parent_id: nil`
|
|
20
|
+
to promote a Community to the top of the tree — the same way
|
|
21
|
+
`Community.create(nil)` makes a top-level Community; a `nil` destination
|
|
22
|
+
is rejected by Atlas for Works and Collections. Atlas enforces the
|
|
23
|
+
structural rules (type, cycle, tombstone) server-side and synchronously
|
|
24
|
+
cascades the ancestry index over descendants, surfacing violations as
|
|
25
|
+
`422`. The Work re-parent endpoint is included — Atlas shipped it (the
|
|
26
|
+
plan had flagged it as optional). All three endpoints use the shared
|
|
27
|
+
`parent_id` body key, including the Work one (not `collection_id`).
|
|
28
|
+
|
|
29
|
+
- **`AtlasRb::Work.linked_members(id, nuid: nil, on_behalf_of: nil)`** —
|
|
30
|
+
`GET /works/:id/linked_members`.
|
|
31
|
+
- **`AtlasRb::Work.add_linked_member(work_id, collection_id, nuid: nil, on_behalf_of: nil)`** —
|
|
32
|
+
`POST /works/:id/linked_members` with a `{ collection_id }` body.
|
|
33
|
+
- **`AtlasRb::Work.remove_linked_member(work_id, collection_id, nuid: nil, on_behalf_of: nil)`** —
|
|
34
|
+
`DELETE /works/:id/linked_members/:collection_id` (Collection as a path
|
|
35
|
+
segment).
|
|
36
|
+
|
|
37
|
+
The DAG overlay: a Work has one structural parent (`a_member_of`) but
|
|
38
|
+
may additionally be a *linked* member of any number of other Collections
|
|
39
|
+
(`a_linked_member_of`). These manage that overlay without moving the
|
|
40
|
+
Work. All three return the Work's current linked Collection noids as a
|
|
41
|
+
bare array (mirroring `Collection.children`); the two mutations return
|
|
42
|
+
the list *after* the change, so no follow-up GET is needed.
|
|
43
|
+
|
|
44
|
+
Cerberus consumes these from the re-parent and "add to collection" UI.
|
|
45
|
+
|
|
3
46
|
## 1.1.1
|
|
4
47
|
|
|
5
48
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
atlas_rb (1.
|
|
4
|
+
atlas_rb (1.2.0)
|
|
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.7)
|
|
27
27
|
logger (1.7.0)
|
|
28
28
|
multipart-post (2.4.1)
|
|
29
29
|
net-http (0.9.1)
|
data/README.md
CHANGED
|
@@ -205,6 +205,47 @@ result["events"].first["action"] # => "update"
|
|
|
205
205
|
Authorization errors (`401` / `403`) are not caught here — they surface as
|
|
206
206
|
raw Faraday responses for the calling application's rescue layer.
|
|
207
207
|
|
|
208
|
+
### Re-parenting
|
|
209
|
+
|
|
210
|
+
`reparent` moves a resource to a new structural parent, binding Atlas's
|
|
211
|
+
`PATCH /<type>/:id/parent` endpoint. It mirrors `create`'s "single parent
|
|
212
|
+
id" shape and returns the updated resource (same shape as `find`), so the
|
|
213
|
+
caller sees the new `a_member_of` without a follow-up GET. Atlas enforces
|
|
214
|
+
the structural rules (type, cycle, tombstone guards) server-side and
|
|
215
|
+
synchronously cascades the ancestry index over descendants; rule
|
|
216
|
+
violations come back as `422`.
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
AtlasRb::Collection.reparent("col-456", "c-999") # move collection to community c-999
|
|
220
|
+
AtlasRb::Work.reparent("w-789", "col-999") # move work to collection col-999
|
|
221
|
+
AtlasRb::Community.reparent("c-123", "c-999") # nest community under c-999
|
|
222
|
+
AtlasRb::Community.reparent("c-123", nil) # promote community to top of tree
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Only `Community.reparent` accepts a `nil` destination (move to the top of
|
|
226
|
+
the tree) — the same way `Community.create(nil)` makes a top-level
|
|
227
|
+
community. A `nil` destination for a Work or Collection is rejected by
|
|
228
|
+
Atlas.
|
|
229
|
+
|
|
230
|
+
### Linked members (the DAG overlay)
|
|
231
|
+
|
|
232
|
+
A Work has exactly one structural parent (`a_member_of`, set by `create` /
|
|
233
|
+
`reparent`) but may additionally be a *linked* member of any number of
|
|
234
|
+
other Collections (`a_linked_member_of`). The linked-member bindings on
|
|
235
|
+
`Work` manage that overlay without ever moving the Work:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
AtlasRb::Work.linked_members("w-789") # => ["col-456", "col-457"]
|
|
239
|
+
AtlasRb::Work.add_linked_member("w-789", "col-456") # => ["col-456"] (updated list)
|
|
240
|
+
AtlasRb::Work.remove_linked_member("w-789", "col-456") # => [] (updated list)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
All three return the Work's current linked Collection noids as a bare
|
|
244
|
+
array (mirroring `Collection.children`); the two mutations return the list
|
|
245
|
+
*after* the change, so no follow-up `linked_members` GET is needed.
|
|
246
|
+
Resolving those Collections' full contents is a Cerberus/Solr concern —
|
|
247
|
+
this gem never queries the index.
|
|
248
|
+
|
|
208
249
|
## End-to-end example
|
|
209
250
|
|
|
210
251
|
JSON responses come back as `AtlasRb::Mash` (a `Hashie::Mash` subclass), so
|
data/lib/atlas_rb/collection.rb
CHANGED
|
@@ -65,6 +65,39 @@ module AtlasRb
|
|
|
65
65
|
find(result["id"], nuid: nuid, on_behalf_of: on_behalf_of)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# Move a Collection to a different parent Community.
|
|
69
|
+
#
|
|
70
|
+
# Wraps `PATCH /collections/<id>/parent` with a `parent_id` of the new
|
|
71
|
+
# Community. Atlas re-parents the Collection and synchronously cascades
|
|
72
|
+
# the ancestry index over its Works; the structural rules (type, cycle,
|
|
73
|
+
# tombstone guards) are enforced server-side and surface as a `422`.
|
|
74
|
+
#
|
|
75
|
+
# Mirrors {.create}'s "single parent id" shape — same kwarg threading,
|
|
76
|
+
# the only difference is the verb and that the Collection already exists.
|
|
77
|
+
#
|
|
78
|
+
# @param id [String] the Collection ID to move.
|
|
79
|
+
# @param new_parent_id [String] the destination Community ID.
|
|
80
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
81
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
82
|
+
# tokens still resolve without it.
|
|
83
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
84
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
85
|
+
# omitted.
|
|
86
|
+
# @return [Hash] the updated `"collection"` object, already unwrapped —
|
|
87
|
+
# the same shape {.find} returns, reflecting the new `a_member_of`.
|
|
88
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
89
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
90
|
+
# `error: "stale_resource"`).
|
|
91
|
+
#
|
|
92
|
+
# @example
|
|
93
|
+
# AtlasRb::Collection.reparent("col-456", "c-999")
|
|
94
|
+
def self.reparent(id, new_parent_id, nuid: nil, on_behalf_of: nil)
|
|
95
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
96
|
+
connection({ parent_id: new_parent_id }, nuid, on_behalf_of: on_behalf_of)
|
|
97
|
+
.patch(ROUTE + id + '/parent')&.body
|
|
98
|
+
))["collection"]
|
|
99
|
+
end
|
|
100
|
+
|
|
68
101
|
# Tombstone (withdraw) a Collection.
|
|
69
102
|
#
|
|
70
103
|
# The Collection remains in Atlas storage but is marked as withdrawn:
|
|
@@ -173,6 +206,9 @@ module AtlasRb
|
|
|
173
206
|
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
174
207
|
# tokens still resolve without it.
|
|
175
208
|
# @return [AtlasRb::Mash] the parsed JSON response.
|
|
209
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
210
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
211
|
+
# `error: "stale_resource"`).
|
|
176
212
|
#
|
|
177
213
|
# @example
|
|
178
214
|
# AtlasRb::Collection.set_thumbnails(
|
data/lib/atlas_rb/community.rb
CHANGED
|
@@ -69,6 +69,45 @@ module AtlasRb
|
|
|
69
69
|
find(result["id"], nuid: nuid, on_behalf_of: on_behalf_of)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# Move a Community to a different parent Community — or to the top of the
|
|
73
|
+
# tree.
|
|
74
|
+
#
|
|
75
|
+
# Wraps `PATCH /communities/<id>/parent` with a `parent_id` of the new
|
|
76
|
+
# parent Community. Pass `new_parent_id = nil` to promote the Community to
|
|
77
|
+
# a top-level node (no parent) — mirroring how {.create} treats a `nil`
|
|
78
|
+
# `id`; the gem omits the blank param and Atlas reads it as "move to top".
|
|
79
|
+
# Atlas re-parents the Community and synchronously cascades the ancestry
|
|
80
|
+
# index over its descendant Collections and Works; the structural rules
|
|
81
|
+
# (cycle, tombstone guards) are enforced server-side and surface as a
|
|
82
|
+
# `422`.
|
|
83
|
+
#
|
|
84
|
+
# @param id [String] the Community ID to move.
|
|
85
|
+
# @param new_parent_id [String, nil] the destination Community ID, or
|
|
86
|
+
# `nil` to move the Community to the top of the tree.
|
|
87
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
88
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
89
|
+
# tokens still resolve without it.
|
|
90
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
91
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
92
|
+
# omitted.
|
|
93
|
+
# @return [Hash] the updated `"community"` object, already unwrapped —
|
|
94
|
+
# the same shape {.find} returns, reflecting the new `a_member_of`.
|
|
95
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
96
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
97
|
+
# `error: "stale_resource"`).
|
|
98
|
+
#
|
|
99
|
+
# @example Move under another Community
|
|
100
|
+
# AtlasRb::Community.reparent("c-123", "c-999")
|
|
101
|
+
#
|
|
102
|
+
# @example Promote to a top-level Community
|
|
103
|
+
# AtlasRb::Community.reparent("c-123", nil)
|
|
104
|
+
def self.reparent(id, new_parent_id, nuid: nil, on_behalf_of: nil)
|
|
105
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
106
|
+
connection({ parent_id: new_parent_id }, nuid, on_behalf_of: on_behalf_of)
|
|
107
|
+
.patch(ROUTE + id + '/parent')&.body
|
|
108
|
+
))["community"]
|
|
109
|
+
end
|
|
110
|
+
|
|
72
111
|
# Tombstone (withdraw) a Community.
|
|
73
112
|
#
|
|
74
113
|
# The Community remains in Atlas storage but is marked as withdrawn:
|
|
@@ -179,6 +218,9 @@ module AtlasRb
|
|
|
179
218
|
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
180
219
|
# tokens still resolve without it.
|
|
181
220
|
# @return [AtlasRb::Mash] the parsed JSON response.
|
|
221
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
222
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
223
|
+
# `error: "stale_resource"`).
|
|
182
224
|
#
|
|
183
225
|
# @example
|
|
184
226
|
# AtlasRb::Community.set_thumbnails(
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtlasRb
|
|
4
|
+
# Base error for atlas_rb. Subclassed for specific wire-level conditions
|
|
5
|
+
# that callers want to handle distinctly. Most non-2xx responses still
|
|
6
|
+
# flow through as Mashes today; we mint typed exceptions only where
|
|
7
|
+
# callers genuinely need to discriminate (currently: optimistic-lock
|
|
8
|
+
# conflicts that ActiveJob `retry_on` policies need to key on).
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Raised when Atlas responds with HTTP 409 + `error: "stale_resource"`,
|
|
12
|
+
# indicating an optimistic-lock conflict that either (a) exhausted
|
|
13
|
+
# Atlas's internal retry budget for a retry-safe action, or (b) hit a
|
|
14
|
+
# retry-unsafe action and surfaced immediately.
|
|
15
|
+
#
|
|
16
|
+
# Callers (typically ActiveJob subclasses in Cerberus) handle this via:
|
|
17
|
+
#
|
|
18
|
+
# retry_on AtlasRb::StaleResourceError, attempts: 5, wait: :polynomially_longer
|
|
19
|
+
#
|
|
20
|
+
# The exception carries the resource_id and action from Atlas's envelope
|
|
21
|
+
# so failure logs are useful without needing the full HTTP response.
|
|
22
|
+
class StaleResourceError < Error
|
|
23
|
+
# @return [String, nil] the conflicted resource's ID, from the envelope.
|
|
24
|
+
attr_reader :resource_id
|
|
25
|
+
|
|
26
|
+
# @return [String, nil] the controller action that conflicted, from the
|
|
27
|
+
# envelope (e.g. `"update_thumbnails"`).
|
|
28
|
+
attr_reader :action
|
|
29
|
+
|
|
30
|
+
# @param message [String] human-readable conflict description.
|
|
31
|
+
# @param resource_id [String, nil] the conflicted resource's ID.
|
|
32
|
+
# @param action [String, nil] the controller action that conflicted.
|
|
33
|
+
def initialize(message, resource_id: nil, action: nil)
|
|
34
|
+
super(message)
|
|
35
|
+
@resource_id = resource_id
|
|
36
|
+
@action = action
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -59,6 +59,7 @@ module AtlasRb
|
|
|
59
59
|
params: params,
|
|
60
60
|
headers: headers
|
|
61
61
|
) do |f|
|
|
62
|
+
f.use AtlasRb::Middleware::RaiseOnStaleResource
|
|
62
63
|
f.response :follow_redirects
|
|
63
64
|
f.adapter Faraday.default_adapter
|
|
64
65
|
end
|
|
@@ -103,6 +104,7 @@ module AtlasRb
|
|
|
103
104
|
url: ENV.fetch("ATLAS_URL", nil),
|
|
104
105
|
headers: headers
|
|
105
106
|
) do |f|
|
|
107
|
+
f.use AtlasRb::Middleware::RaiseOnStaleResource
|
|
106
108
|
f.request :multipart
|
|
107
109
|
f.request :url_encoded
|
|
108
110
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtlasRb
|
|
4
|
+
# Faraday middleware namespace.
|
|
5
|
+
module Middleware
|
|
6
|
+
# Translates Atlas's structured optimistic-lock conflict response into a
|
|
7
|
+
# typed Ruby exception.
|
|
8
|
+
#
|
|
9
|
+
# Atlas surfaces an exhausted-retry (or retry-unsafe) optimistic-lock
|
|
10
|
+
# conflict as an HTTP `409 Conflict` whose JSON body carries the
|
|
11
|
+
# discriminator `error: "stale_resource"`. This middleware keys on the
|
|
12
|
+
# **status + discriminator pair** and raises {AtlasRb::StaleResourceError},
|
|
13
|
+
# carrying the envelope's `resource_id` and `action` through so callers'
|
|
14
|
+
# failure logs are useful without the full response.
|
|
15
|
+
#
|
|
16
|
+
# It is intentionally narrow: any other status, or a 409 without the
|
|
17
|
+
# discriminator, passes through untouched so the caller still sees the
|
|
18
|
+
# response as a Mash (see {AtlasRb::StaleResourceError} for the rationale —
|
|
19
|
+
# atlas_rb stays a thin Faraday binding and translates only the one wire
|
|
20
|
+
# signal Cerberus jobs need to discriminate on).
|
|
21
|
+
class RaiseOnStaleResource < Faraday::Middleware
|
|
22
|
+
# @param env [Faraday::Env] the completed response environment.
|
|
23
|
+
# @raise [AtlasRb::StaleResourceError] on a 409 whose body carries
|
|
24
|
+
# `error: "stale_resource"`.
|
|
25
|
+
# @return [void]
|
|
26
|
+
def on_complete(env)
|
|
27
|
+
return unless env.status == 409
|
|
28
|
+
|
|
29
|
+
body = parse_json(env.body)
|
|
30
|
+
return unless body.is_a?(Hash) && body["error"] == "stale_resource"
|
|
31
|
+
|
|
32
|
+
raise AtlasRb::StaleResourceError.new(
|
|
33
|
+
body["message"] || "Atlas reported a stale-resource conflict",
|
|
34
|
+
resource_id: body["resource_id"],
|
|
35
|
+
action: body["action"]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def parse_json(body)
|
|
42
|
+
return body if body.is_a?(Hash)
|
|
43
|
+
|
|
44
|
+
JSON.parse(body.to_s)
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/atlas_rb/work.rb
CHANGED
|
@@ -129,6 +129,43 @@ module AtlasRb
|
|
|
129
129
|
find(result["id"], nuid: nuid, on_behalf_of: on_behalf_of)
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
+
# Move a Work to a different parent Collection.
|
|
133
|
+
#
|
|
134
|
+
# Wraps `PATCH /works/<id>/parent` with a `parent_id` of the new
|
|
135
|
+
# Collection. This changes the Work's single **structural** home
|
|
136
|
+
# (`a_member_of`) — distinct from {.add_linked_member}, which adds an
|
|
137
|
+
# additional *linked* membership without moving the Work. Atlas
|
|
138
|
+
# re-parents the Work and synchronously updates its ancestry index; the
|
|
139
|
+
# structural rules (type, cycle, tombstone guards) are enforced
|
|
140
|
+
# server-side and surface as a `422`.
|
|
141
|
+
#
|
|
142
|
+
# **Note**: like {.create}, the destination here is a **Collection**, but
|
|
143
|
+
# the underlying request still uses the shared `parent_id` body key (not
|
|
144
|
+
# `collection_id`) — every re-parent endpoint posts `{ parent_id }`.
|
|
145
|
+
#
|
|
146
|
+
# @param id [String] the Work ID to move.
|
|
147
|
+
# @param new_collection_id [String] the destination Collection ID.
|
|
148
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
149
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
150
|
+
# tokens still resolve without it.
|
|
151
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
152
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
153
|
+
# omitted.
|
|
154
|
+
# @return [Hash] the updated `"work"` object, already unwrapped — the
|
|
155
|
+
# same shape {.find} returns, reflecting the new `a_member_of`.
|
|
156
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
157
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
158
|
+
# `error: "stale_resource"`).
|
|
159
|
+
#
|
|
160
|
+
# @example
|
|
161
|
+
# AtlasRb::Work.reparent("w-789", "col-999")
|
|
162
|
+
def self.reparent(id, new_collection_id, nuid: nil, on_behalf_of: nil)
|
|
163
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
164
|
+
connection({ parent_id: new_collection_id }, nuid, on_behalf_of: on_behalf_of)
|
|
165
|
+
.patch(ROUTE + id + '/parent')&.body
|
|
166
|
+
))["work"]
|
|
167
|
+
end
|
|
168
|
+
|
|
132
169
|
# Tombstone (withdraw) a Work.
|
|
133
170
|
#
|
|
134
171
|
# The Work remains in Atlas storage along with its FileSets and Blobs,
|
|
@@ -170,6 +207,9 @@ module AtlasRb
|
|
|
170
207
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
171
208
|
# omitted.
|
|
172
209
|
# @return [Faraday::Response] the raw response. Status `200` on success.
|
|
210
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
211
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
212
|
+
# `error: "stale_resource"`).
|
|
173
213
|
#
|
|
174
214
|
# @example
|
|
175
215
|
# AtlasRb::Work.complete("w-789")
|
|
@@ -243,6 +283,9 @@ module AtlasRb
|
|
|
243
283
|
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
244
284
|
# tokens still resolve without it.
|
|
245
285
|
# @return [AtlasRb::Mash] the parsed JSON response.
|
|
286
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
287
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
288
|
+
# `error: "stale_resource"`).
|
|
246
289
|
#
|
|
247
290
|
# @example
|
|
248
291
|
# AtlasRb::Work.set_thumbnails(
|
|
@@ -276,6 +319,9 @@ module AtlasRb
|
|
|
276
319
|
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
277
320
|
# tokens still resolve without it.
|
|
278
321
|
# @return [AtlasRb::Mash] the parsed JSON response.
|
|
322
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
323
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
324
|
+
# `error: "stale_resource"`).
|
|
279
325
|
#
|
|
280
326
|
# @example
|
|
281
327
|
# AtlasRb::Work.set_image_derivatives(
|
|
@@ -340,5 +386,101 @@ module AtlasRb
|
|
|
340
386
|
ROUTE + id + '/mods' + (kind.present? ? ".#{kind}" : '')
|
|
341
387
|
)&.body
|
|
342
388
|
end
|
|
389
|
+
|
|
390
|
+
# List the Collections a Work is a *linked* member of.
|
|
391
|
+
#
|
|
392
|
+
# Wraps `GET /works/<id>/linked_members`. Linked membership is the DAG
|
|
393
|
+
# overlay — a Work has exactly one structural parent (`a_member_of`, set
|
|
394
|
+
# by {.create} / {.reparent}) but may additionally appear in any number
|
|
395
|
+
# of other Collections as a linked member (`a_linked_member_of`). This
|
|
396
|
+
# returns just those linked Collection noids; the structural parent is
|
|
397
|
+
# not included.
|
|
398
|
+
#
|
|
399
|
+
# @param id [String] the Work ID.
|
|
400
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
401
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
402
|
+
# tokens still resolve without it.
|
|
403
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
404
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
405
|
+
# omitted.
|
|
406
|
+
# @return [Array<String>] linked Collection noids (possibly empty). The
|
|
407
|
+
# shape mirrors {Collection.children} — a bare array of ids, not an
|
|
408
|
+
# envelope.
|
|
409
|
+
#
|
|
410
|
+
# @example
|
|
411
|
+
# AtlasRb::Work.linked_members("w-789")
|
|
412
|
+
# # => ["col-456", "col-457"]
|
|
413
|
+
def self.linked_members(id, nuid: nil, on_behalf_of: nil)
|
|
414
|
+
JSON.parse(
|
|
415
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/linked_members')&.body
|
|
416
|
+
)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Add a linked membership: surface a Work in an additional Collection.
|
|
420
|
+
#
|
|
421
|
+
# Wraps `POST /works/<id>/linked_members` with a `collection_id` body.
|
|
422
|
+
# This does **not** move the Work — its structural parent (`a_member_of`)
|
|
423
|
+
# is untouched; the Collection is added to `a_linked_member_of`. Atlas
|
|
424
|
+
# enforces two-sided authorization (edit on the Work *and* the target
|
|
425
|
+
# Collection) and the structural guards, surfacing failures as a `422`.
|
|
426
|
+
# Permissions are never changed by this call.
|
|
427
|
+
#
|
|
428
|
+
# @param work_id [String] the Work ID.
|
|
429
|
+
# @param collection_id [String] the Collection to link the Work into.
|
|
430
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
431
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
432
|
+
# tokens still resolve without it.
|
|
433
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
434
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
435
|
+
# omitted.
|
|
436
|
+
# @return [Array<String>] the Work's full set of linked Collection noids
|
|
437
|
+
# *after* the add — the affected sub-resource, so no follow-up
|
|
438
|
+
# {.linked_members} GET is needed.
|
|
439
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
440
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
441
|
+
# `error: "stale_resource"`).
|
|
442
|
+
#
|
|
443
|
+
# @example
|
|
444
|
+
# AtlasRb::Work.add_linked_member("w-789", "col-456")
|
|
445
|
+
# # => ["col-456"]
|
|
446
|
+
def self.add_linked_member(work_id, collection_id, nuid: nil, on_behalf_of: nil)
|
|
447
|
+
JSON.parse(
|
|
448
|
+
connection({ collection_id: collection_id }, nuid, on_behalf_of: on_behalf_of)
|
|
449
|
+
.post(ROUTE + work_id + '/linked_members')&.body
|
|
450
|
+
)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Remove a linked membership: drop a Work from an additional Collection.
|
|
454
|
+
#
|
|
455
|
+
# Wraps `DELETE /works/<id>/linked_members/<collection_id>` — the
|
|
456
|
+
# Collection is passed as a path segment, not a body. This removes the
|
|
457
|
+
# Collection from the Work's `a_linked_member_of`; the structural parent
|
|
458
|
+
# (`a_member_of`) is untouched. Atlas enforces the same two-sided
|
|
459
|
+
# authorization as {.add_linked_member}. Removing a link that does not
|
|
460
|
+
# exist is a server-side concern; this binding simply forwards the call.
|
|
461
|
+
#
|
|
462
|
+
# @param work_id [String] the Work ID.
|
|
463
|
+
# @param collection_id [String] the linked Collection to remove.
|
|
464
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
465
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
466
|
+
# tokens still resolve without it.
|
|
467
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
468
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
469
|
+
# omitted.
|
|
470
|
+
# @return [Array<String>] the Work's remaining linked Collection noids
|
|
471
|
+
# *after* the removal (possibly empty).
|
|
472
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
473
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
474
|
+
# `error: "stale_resource"`).
|
|
475
|
+
#
|
|
476
|
+
# @example
|
|
477
|
+
# AtlasRb::Work.remove_linked_member("w-789", "col-456")
|
|
478
|
+
# # => []
|
|
479
|
+
def self.remove_linked_member(work_id, collection_id, nuid: nil, on_behalf_of: nil)
|
|
480
|
+
JSON.parse(
|
|
481
|
+
connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
482
|
+
.delete(ROUTE + work_id + '/linked_members/' + collection_id)&.body
|
|
483
|
+
)
|
|
484
|
+
end
|
|
343
485
|
end
|
|
344
486
|
end
|
data/lib/atlas_rb.rb
CHANGED
|
@@ -4,7 +4,9 @@ require "faraday"
|
|
|
4
4
|
require "faraday/multipart"
|
|
5
5
|
require "faraday/follow_redirects"
|
|
6
6
|
require_relative "atlas_rb/version"
|
|
7
|
+
require_relative "atlas_rb/errors"
|
|
7
8
|
require_relative "atlas_rb/configuration"
|
|
9
|
+
require_relative "atlas_rb/middleware/raise_on_stale_resource"
|
|
8
10
|
require_relative "atlas_rb/faraday_helper"
|
|
9
11
|
require_relative "atlas_rb/mash"
|
|
10
12
|
require_relative "atlas_rb/authentication"
|
|
@@ -69,10 +71,8 @@ require_relative "atlas_rb/system/user"
|
|
|
69
71
|
# AtlasRb::Blob.content(blob["id"]) { |chunk| f.write(chunk) }
|
|
70
72
|
# end
|
|
71
73
|
module AtlasRb
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
# objects or `JSON::ParserError`s on malformed bodies.
|
|
75
|
-
class Error < StandardError; end
|
|
74
|
+
# The error hierarchy ({AtlasRb::Error}, {AtlasRb::StaleResourceError}) lives
|
|
75
|
+
# in `atlas_rb/errors.rb`, required above.
|
|
76
76
|
|
|
77
77
|
# The gem-wide configuration instance. Lazily initialized — host
|
|
78
78
|
# applications register defaults via {AtlasRb.configure}.
|
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.
|
|
4
|
+
version: 1.2.0
|
|
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-
|
|
11
|
+
date: 2026-06-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -125,9 +125,11 @@ files:
|
|
|
125
125
|
- lib/atlas_rb/community.rb
|
|
126
126
|
- lib/atlas_rb/configuration.rb
|
|
127
127
|
- lib/atlas_rb/delegate.rb
|
|
128
|
+
- lib/atlas_rb/errors.rb
|
|
128
129
|
- lib/atlas_rb/faraday_helper.rb
|
|
129
130
|
- lib/atlas_rb/file_set.rb
|
|
130
131
|
- lib/atlas_rb/mash.rb
|
|
132
|
+
- lib/atlas_rb/middleware/raise_on_stale_resource.rb
|
|
131
133
|
- lib/atlas_rb/resource.rb
|
|
132
134
|
- lib/atlas_rb/system/user.rb
|
|
133
135
|
- lib/atlas_rb/version.rb
|