atlas_rb 1.1.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 195a3c85a8653e6835bb472b7cda45847e254057075694e37ecfa779efe2dd39
4
- data.tar.gz: ea58c157df06abcfa8d3f4c6b18fd00e5f87892a8b4231b526ac3e60a8e13510
3
+ metadata.gz: 93e0688ad6097a3b6a36ed26db8c19d3416cb3c59beb3d95dd3cff61c6cdd176
4
+ data.tar.gz: 1af0ff155d288412bd6ab8a95fc50da55cfccd02d605b5bae1cf28cebdf33973
5
5
  SHA512:
6
- metadata.gz: 2782eeb3e29454728e68e85144ba5046dd86d9b2ff58eea1cfd704bf2ce03e70f81e977a5b9ad5ffe4091d69d99225746ed2076e875891f358308038983cbfe6
7
- data.tar.gz: 8dc180f51b17ffdb1feec818445573ec9c25eab3ee4e4245e9afb31f564a14dfbc93db40e3bd174a078e89f31bd57c240eb85b17f69828494ebaab3c0f23f9f4
6
+ metadata.gz: 44c8582605f238861c424a769fd4e788e08189edd969f3974d113a938f54ecce522e143ec0418cd813de958c1cbb6d9cd44b4796b6b9c190629af49a1d0417e9
7
+ data.tar.gz: 8ad34a6b44d4472ba300f8ef7b6c9f30d5d8674f0f69f7711e094d202ba07f2b428607b463ce8261e21e2fa5bbcbca865da9de3fb3af8e28d6e8906810037a12
data/.version CHANGED
@@ -1 +1 @@
1
- 1.1.2
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.1.2)
4
+ atlas_rb (1.2.0)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 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
@@ -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:
@@ -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:
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,
@@ -349,5 +386,101 @@ module AtlasRb
349
386
  ROUTE + id + '/mods' + (kind.present? ? ".#{kind}" : '')
350
387
  )&.body
351
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
352
485
  end
353
486
  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.1.2
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-05-29 00:00:00.000000000 Z
11
+ date: 2026-06-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday