atlas_rb 1.10.1 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d8edc13167cb16fb5482d6a5cd19760bf323f67bf1a23f00a9508f8320bf798
4
- data.tar.gz: e86459ce460b8eb30cffcfaaed75b06c750649aa57559e33d5ea277ce0bd2d8c
3
+ metadata.gz: 7bacb5eb215d1dd6d3df935abd82d74909ca81d1ce91f44de67f370846dbbd80
4
+ data.tar.gz: c6ea26efb6d7b083d2ede1455a126901bae8f446ece2997b441d7e593fdaba51
5
5
  SHA512:
6
- metadata.gz: 06a66bf273c655ae075c821d158555139094714e3b5d0f3ca15296657afe067267ef781190c4804d5a97d3576237a11d664141f28256702df4151fefde515255
7
- data.tar.gz: dfd113525542142a75e0789a5e9ed34424485e1ac69359f5e4c505e24d3752e9eda54e756804f13f195588f6079d0c828426e0a5b937d8ad53c2ef8708eaff00
6
+ metadata.gz: a9cc966ec7e7505b26ca5e5adaeacb78201b8f83c141d77a63c6ece959fce7de3c421e26274b7b943b964ad43ff32899a3da189ac4d9fdaebf43ed7cb9e5249c
7
+ data.tar.gz: 366a253542dfcf24f9e44ba4482c013ff426ec9b361b56636c2e2b96020a9b1ca738a93fa6f97d696b3b8bbd2e873ae761c7ceb4f8b8a5ef086bcf7c93ba2b5d
data/.version CHANGED
@@ -1 +1 @@
1
- 1.10.1
1
+ 1.11.1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.11.0
4
+
5
+ ### Added — `Work.associations` / `.associate` / `.disassociate`
6
+
7
+ DRS v1 let a depositor declare that one object is the codebook, figure,
8
+ transcription, instructional material or supplemental material **for** another.
9
+ The two objects stayed separate records, and each one's page showed the link
10
+ from its own end. Atlas now records this again, and these three bindings reach
11
+ it.
12
+
13
+ ```ruby
14
+ AtlasRb::Work.associate(codebook_id, dataset_id, type: "is_codebook_for")
15
+ AtlasRb::Work.associations(dataset_id)
16
+ # => {"outbound" => {}, "inbound" => {"is_codebook_for" => ["w-codebook"]}}
17
+ AtlasRb::Work.disassociate(codebook_id, dataset_id, type: "is_codebook_for")
18
+ ```
19
+
20
+ All three return the same shape, so a mutation needs no follow-up read.
21
+ `outbound` is what the Work asserts; `inbound` is what other Works assert about
22
+ it. The edge is stored once, on the asserting Work, and Atlas derives the other
23
+ direction — so the two can never drift apart.
24
+
25
+ The five predicates are in `AtlasRb::Work::ASSOCIATION_TYPES`, for building a
26
+ select box without hard-coding the vocabulary. A sixth needs an Atlas release,
27
+ so the list cannot get ahead of the server.
28
+
29
+ Writes are admin / devolved-admin only: the claim renders on the target's page
30
+ too, and the asserter often holds no rights over it.
31
+
32
+ ### Added — `WorkAssociationError`
33
+
34
+ A `422` on an association path now raises a typed error carrying `#code`
35
+ (`invalid_type`, `target_not_found`, `invalid_target_type`,
36
+ `self_association`, `tombstoned_work`, `tombstoned_target`). A `403` raises the
37
+ existing `ForbiddenError`. Without these the binding would return the envelope
38
+ as if it were a success and the rejection would be silently discarded.
39
+
40
+ ### Changed — the `destroy` docstrings now describe a purge
41
+
42
+ `Admin::{Work,Collection,Community}.destroy`, `FileSet.destroy` and
43
+ `Blob.destroy` previously promised more than the server did: the docstrings
44
+ claimed a cascade and removal "from Atlas storage" while the server deleted one
45
+ Postgres row and one Solr document, leaving every byte on disk.
46
+
47
+ Atlas has since made `destroy` mean what the docstrings said, and they are
48
+ updated to match what it now does — including the parts they never mentioned:
49
+
50
+ - Deleting a Blob or a FileSet removes the **whole OCFL object**, so every
51
+ retained revision goes, not only the current one. `versions` and `rollback`
52
+ have nothing left to work with afterwards.
53
+ - `Admin::Collection.destroy` and `Admin::Community.destroy` refuse with a
54
+ `422` (`has_children`) while the container still holds a member, **including
55
+ a tombstoned one**. That is stricter than `tombstone`, which counts only live
56
+ members: a purge cannot be undone, so a member left behind is orphaned for
57
+ good.
58
+
59
+ No signature changed; this release is additive.
60
+
3
61
  ## 1.10.1
4
62
 
5
63
  ### Added — `Work.mark_incomplete` / `Work.clear_incomplete`
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (1.10.1)
4
+ atlas_rb (1.11.1)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
@@ -13,10 +13,18 @@ module AtlasRb
13
13
  # @api private
14
14
  ROUTE = "/collections/"
15
15
 
16
- # Hard-delete a Collection.
16
+ # Hard-delete a Collection — a purge, not a withdrawal.
17
17
  #
18
+ # Removes the Collection's metadata, its descriptive-metadata
19
+ # FileSet, and the OCFL objects holding the preserved bytes.
18
20
  # Unrecoverable — prefer {AtlasRb::Collection.tombstone} for
19
- # user-visible withdrawal. Operator-only.
21
+ # user-visible withdrawal. Admin-only.
22
+ #
23
+ # Atlas refuses with a `422` (`has_children`) while the Collection
24
+ # still holds a Work or a sub-container, **including tombstoned
25
+ # ones**. That is stricter than tombstone, which counts only live
26
+ # members: a member left behind by a purge is orphaned for good.
27
+ # Empty the tree leaf-first.
20
28
  #
21
29
  # @param id [String] the Collection ID.
22
30
  # @param confirm [Symbol] must be `:i_understand`. Any other value
@@ -13,10 +13,18 @@ module AtlasRb
13
13
  # @api private
14
14
  ROUTE = "/communities/"
15
15
 
16
- # Hard-delete a Community.
16
+ # Hard-delete a Community — a purge, not a withdrawal.
17
17
  #
18
+ # Removes the Community's metadata, its descriptive-metadata
19
+ # FileSet, and the OCFL objects holding the preserved bytes.
18
20
  # Unrecoverable — prefer {AtlasRb::Community.tombstone} for
19
- # user-visible withdrawal. Operator-only.
21
+ # user-visible withdrawal. Admin-only.
22
+ #
23
+ # Atlas refuses with a `422` (`has_children`) while the Community
24
+ # still holds a Collection or a sub-community, **including
25
+ # tombstoned ones**. That is stricter than tombstone, which counts
26
+ # only live members: a member left behind by a purge is orphaned
27
+ # for good. Empty the tree leaf-first.
20
28
  #
21
29
  # @param id [String] the Community ID.
22
30
  # @param confirm [Symbol] must be `:i_understand`. Any other value
@@ -13,11 +13,16 @@ module AtlasRb
13
13
  # @api private
14
14
  ROUTE = "/works/"
15
15
 
16
- # Hard-delete a Work.
16
+ # Hard-delete a Work — a purge, not a withdrawal.
17
17
  #
18
- # Removes the Work, its FileSets, and their Blobs from Atlas
19
- # storage. Unrecoverable — prefer {AtlasRb::Work.tombstone} for
20
- # the user-visible withdrawal path. This is operator-only.
18
+ # Removes the Work's metadata, cascades into its FileSets and their
19
+ # Blobs, and removes the OCFL objects holding the preserved bytes:
20
+ # every retained revision, not only the current one. Nothing
21
+ # survives but the audit row, which records the NOIDs it removed.
22
+ #
23
+ # Unrecoverable — prefer {AtlasRb::Work.tombstone} for the
24
+ # user-visible withdrawal path, which keeps everything and can be
25
+ # reversed. Admin-only.
21
26
  #
22
27
  # @param id [String] the Work ID.
23
28
  # @param confirm [Symbol] must be `:i_understand`. Any other value
data/lib/atlas_rb/blob.rb CHANGED
@@ -186,7 +186,12 @@ module AtlasRb
186
186
  end
187
187
  end
188
188
 
189
- # Delete a Blob (the bytes *and* the metadata record).
189
+ # Delete a Blob: the metadata record **and** the bytes.
190
+ #
191
+ # Atlas removes the whole OCFL object, so **every retained revision** goes,
192
+ # not only the current one — {.versions} and {.rollback} have nothing left
193
+ # to work with afterwards. Unrecoverable, and admin-only. The Blob is also
194
+ # unlinked from its FileSet, whose METS is rebuilt.
190
195
  #
191
196
  # @param id [String] the Blob ID.
192
197
  # @param nuid [String, nil] optional acting user's NUID. On the relay-signing
@@ -101,6 +101,38 @@ module AtlasRb
101
101
  end
102
102
  end
103
103
 
104
+ # Raised when Atlas rejects a Work-association write
105
+ # (`POST` / `DELETE /works/:id/associations...`) with a `422` carrying a
106
+ # machine-readable `error` discriminator — `invalid_type` (not one of the
107
+ # five predicates), `target_not_found`, `invalid_target_type` (the target is
108
+ # not a Work), `self_association`, `tombstoned_work`, or `tombstoned_target`.
109
+ #
110
+ # The association sibling of {LinkedMemberError}; same shape, same rationale
111
+ # (the binding would otherwise discard the envelope on a non-2xx).
112
+ #
113
+ # rescue AtlasRb::WorkAssociationError => e
114
+ # flash.now[:alert] = t("associations.errors.#{e.code}", default: e.message)
115
+ #
116
+ # @note Authorization failures surface as {ForbiddenError} (HTTP 403) —
117
+ # asserting an association is admin / devolved-admin only.
118
+ class WorkAssociationError < Error
119
+ # @return [String, nil] the machine-readable error code from the envelope,
120
+ # suitable for keying an i18n map.
121
+ attr_reader :code
122
+
123
+ # @return [String, nil] the rejected Work's ID, from the envelope.
124
+ attr_reader :resource_id
125
+
126
+ # @param message [String] human-readable rejection description.
127
+ # @param code [String, nil] the envelope's `error` discriminator.
128
+ # @param resource_id [String, nil] the rejected Work's ID.
129
+ def initialize(message, code: nil, resource_id: nil)
130
+ super(message)
131
+ @code = code
132
+ @resource_id = resource_id
133
+ end
134
+ end
135
+
104
136
  # Raised when Atlas rejects a Compilation (Set) write with a `422`
105
137
  # carrying a machine-readable `error` discriminator — a blank title on
106
138
  # create/update (`invalid_record`), or a membership add whose noid does
@@ -84,7 +84,12 @@ module AtlasRb
84
84
  ))["file_set"]
85
85
  end
86
86
 
87
- # Delete a FileSet.
87
+ # Delete a FileSet, its Blobs, and their bytes.
88
+ #
89
+ # Atlas cascades into the FileSet's Blobs and removes each one's OCFL
90
+ # object, so every retained revision of those binaries goes too.
91
+ # Unrecoverable, and admin-only. The parent Work's METS structMap is
92
+ # rebuilt without the removed pages.
88
93
  #
89
94
  # @param id [String] the FileSet ID.
90
95
  # @param nuid [String, nil] optional acting user's NUID. On the relay-signing
@@ -15,7 +15,8 @@ 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,
18
+ # (`.../parent`), linked-member (`.../linked_members...`) and association
19
+ # (`.../associations...`) write paths,
19
20
  # the Compilation surface (`/compilations...`), the derivative-permissions
20
21
  # write (`.../derivative_permissions`), the container-create endpoints
21
22
  # ({CREATE_PATHS}), and binary uploads (`/files...`, `/file_sets...`), and
@@ -37,6 +38,7 @@ module AtlasRb
37
38
  # - `403` on a re-parent/linked/Compilation/derivative-permissions/create path → {AtlasRb::ForbiddenError}
38
39
  # - `422` on `.../parent` → {AtlasRb::ReparentError} (`error`/`resource_id`)
39
40
  # - `422` on `.../linked_members...` → {AtlasRb::LinkedMemberError}
41
+ # - `422` on `.../associations...` → {AtlasRb::WorkAssociationError}
40
42
  # - `422` on `/compilations...` → {AtlasRb::CompilationError}
41
43
  # - `422` on `.../derivative_permissions` → {AtlasRb::DerivativePermissionsError}
42
44
  # - `422` + an ACL discriminator anywhere → {AtlasRb::PermissionsError}
@@ -63,6 +65,7 @@ module AtlasRb
63
65
  # @raise [AtlasRb::ForbiddenError] on a 403 to a re-parent/linked/Compilation/create path.
64
66
  # @raise [AtlasRb::ReparentError] on a 422 to a re-parent path.
65
67
  # @raise [AtlasRb::LinkedMemberError] on a 422 to a linked-member path.
68
+ # @raise [AtlasRb::WorkAssociationError] on a 422 to an association path.
66
69
  # @raise [AtlasRb::CompilationError] on a 422 to a Compilation path.
67
70
  # @raise [AtlasRb::DerivativePermissionsError] on a 422 to a derivative-permissions path.
68
71
  # @raise [AtlasRb::PermissionsError] on a 422 carrying an ACL-invariant discriminator.
@@ -74,6 +77,7 @@ module AtlasRb
74
77
  path = env.url&.path.to_s
75
78
  reparent = path.end_with?("/parent")
76
79
  linked = path.include?("/linked_members")
80
+ association = path.include?("/associations")
77
81
  compilation = path.start_with?("/compilations")
78
82
  deriv_perms = path.end_with?("/derivative_permissions")
79
83
  create = env.method.to_s == "post" && CREATE_PATHS.include?(path.chomp("/"))
@@ -92,11 +96,11 @@ module AtlasRb
92
96
  )
93
97
  end
94
98
 
95
- return unless reparent || linked || compilation || deriv_perms || create || upload
99
+ return unless reparent || linked || association || compilation || deriv_perms || create || upload
96
100
 
97
101
  if env.status == 403
98
102
  # 403s on upload paths stay raw — acting-as/authz isn't an upload concern here.
99
- return unless reparent || linked || compilation || deriv_perms || create
103
+ return unless reparent || linked || association || compilation || deriv_perms || create
100
104
 
101
105
  raise AtlasRb::ForbiddenError.new(
102
106
  body["message"] || "Atlas refused the request",
@@ -116,6 +120,12 @@ module AtlasRb
116
120
  code: body["error"],
117
121
  resource_id: body["resource_id"]
118
122
  )
123
+ elsif association
124
+ raise AtlasRb::WorkAssociationError.new(
125
+ body["message"] || "Atlas rejected the association write",
126
+ code: body["error"],
127
+ resource_id: body["resource_id"]
128
+ )
119
129
  elsif compilation
120
130
  raise AtlasRb::CompilationError.new(
121
131
  body["message"] || "Atlas rejected the compilation write",
data/lib/atlas_rb/work.rb CHANGED
@@ -58,7 +58,10 @@ module AtlasRb
58
58
  # omitted.
59
59
  # @return [AtlasRb::Mash] `{ "works" => [...], "pagination" => {...} }`.
60
60
  # Each entry in `"works"` is a Work summary (`id`, `title`,
61
- # `description`, `in_progress`, `incomplete`, `incomplete_reason`).
61
+ # `description`, `in_progress`, `incomplete`, `incomplete_reason`,
62
+ # `handle`). `handle` is carried on the summary, not just the detail
63
+ # read, so "which Works never minted?" is answerable from one page
64
+ # rather than a fetch per row.
62
65
  #
63
66
  # @example Find stuck deposits
64
67
  # AtlasRb::Work.list(in_progress: true)
@@ -220,11 +223,28 @@ module AtlasRb
220
223
  # "stuck" list.
221
224
  #
222
225
  # Idempotent on the server: calling `complete` on an already-complete
223
- # Work is a no-op — Atlas simply re-saves with `in_progress: false`.
226
+ # Work is a no-op — Atlas re-saves with `in_progress: false`.
224
227
  # Atlas does not currently stamp a `completed_by` audit field; the
225
228
  # `nuid:` parameter is plumbed through for parity with the other
226
229
  # lifecycle bindings and in case Atlas adds completion audit later.
227
230
  #
231
+ # **This call also mints the Work's persistent identifier.** Atlas
232
+ # registers `<prefix>/<noid>` with its Handle service, pointed at the
233
+ # public Work page, and records it as `handle` on the Work. Two
234
+ # consequences for a caller:
235
+ #
236
+ # * **Minting can never fail the call.** A handle server that is down,
237
+ # slow or unconfigured leaves `handle` null and the Work still
238
+ # complete — never a non-2xx. So a `200` does not promise a handle:
239
+ # the response body carries the Work, so check `handle` on it rather
240
+ # than assuming success minted one.
241
+ # * **Re-completing is safe.** Atlas mints only when `handle` is empty,
242
+ # and the underlying registration is keyed by handle name, so a repeat
243
+ # call re-points rather than minting a second identifier.
244
+ #
245
+ # A deployment with no handle server configured mints nothing at all,
246
+ # which is the normal state for a stack brought up without it.
247
+ #
228
248
  # @param id [String] the Work ID.
229
249
  # @param nuid [String, nil] optional NUID of the acting user.
230
250
  # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
@@ -799,5 +819,128 @@ module AtlasRb
799
819
  .delete(ROUTE + work_id + '/linked_members/' + collection_id)
800
820
  )
801
821
  end
822
+
823
+ # The five relationship predicates Atlas accepts, carried here so a caller
824
+ # can build a select box without hard-coding the vocabulary. Adding a sixth
825
+ # needs an Atlas release, so this list cannot drift ahead of the server.
826
+ ASSOCIATION_TYPES = %w[
827
+ is_codebook_for
828
+ is_figure_for
829
+ is_instructional_material_for
830
+ is_supplemental_material_for
831
+ is_transcription_of
832
+ ].freeze
833
+
834
+ # List a Work's typed associations with other Works.
835
+ #
836
+ # Wraps `GET /works/<id>/associations`. An association is DRS v1's
837
+ # "associated works": a directed claim that one object is the codebook,
838
+ # figure, transcription, instructional material or supplemental material
839
+ # **for** another. Both objects stay separate records — this is not
840
+ # membership, and nothing moves in the containment tree.
841
+ #
842
+ # The edge is stored once, on the Work that asserts it. Atlas derives the
843
+ # other direction, so `outbound` and `inbound` can never disagree.
844
+ #
845
+ # @param id [String] the Work ID.
846
+ # @param nuid [String, nil] optional acting user's NUID. On the relay-signing
847
+ # path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
848
+ # path it is ignored (identity lives in the token).
849
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
850
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
851
+ # omitted.
852
+ # @return [Hash] `{"outbound" => {predicate => [noid, …]}, "inbound" => {…}}`.
853
+ # `outbound` is what this Work asserts, `inbound` what other Works assert
854
+ # about it. Predicates holding no edges are omitted, so both maps are
855
+ # `{}` for an unassociated Work.
856
+ #
857
+ # @example
858
+ # AtlasRb::Work.associations("w-789")
859
+ # # => {"outbound" => {"is_codebook_for" => ["w-123"]}, "inbound" => {}}
860
+ def self.associations(id, nuid: nil, on_behalf_of: nil)
861
+ JSON.parse(
862
+ connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/associations')&.body
863
+ )
864
+ end
865
+
866
+ # Assert that this Work stands in a typed relationship to another Work.
867
+ #
868
+ # Wraps `POST /works/<id>/associations` with a `work_id` + `type` body.
869
+ # The edge is stored on **this** Work only; `target` reports the same edge
870
+ # under `inbound`. Asserting an edge that already exists is a no-op, and
871
+ # two Works can hold several different edges at once.
872
+ #
873
+ # A cycle is permitted and meaningful — "A is a transcription of B" and
874
+ # "B is a figure for A" can both be true.
875
+ #
876
+ # @param work_id [String] the asserting Work's ID (the codebook, figure, …).
877
+ # @param target_id [String] the Work being pointed at (the dataset, article, …).
878
+ # @param type [String] one of {ASSOCIATION_TYPES}.
879
+ # @param nuid [String, nil] optional acting user's NUID. On the relay-signing
880
+ # path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
881
+ # path it is ignored (identity lives in the token).
882
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
883
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
884
+ # omitted.
885
+ # @return [Hash] the Work's associations *after* the add, in the
886
+ # {.associations} shape — so no follow-up GET is needed.
887
+ # @raise [AtlasRb::WorkAssociationError] if Atlas rejects the claim (HTTP
888
+ # 422): unknown type, unresolvable target, a non-Work target, the Work
889
+ # itself, or either end tombstoned. The envelope's `error` code is
890
+ # exposed as `#code`.
891
+ # @raise [AtlasRb::ForbiddenError] if Atlas refuses the write (HTTP 403).
892
+ # Associating is admin / devolved-admin only, because the claim renders
893
+ # on the target's page too.
894
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no
895
+ # such Work, so the write did not happen.
896
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's
897
+ # status and body.
898
+ #
899
+ # @example
900
+ # AtlasRb::Work.associate("w-789", "w-123", type: "is_codebook_for")
901
+ # # => {"outbound" => {"is_codebook_for" => ["w-123"]}, "inbound" => {}}
902
+ def self.associate(work_id, target_id, type:, nuid: nil, on_behalf_of: nil)
903
+ write_resource(
904
+ connection({ work_id: target_id, type: type }, nuid, on_behalf_of: on_behalf_of)
905
+ .post(ROUTE + work_id + '/associations')
906
+ )
907
+ end
908
+
909
+ # Retract one typed relationship between two Works.
910
+ #
911
+ # Wraps `DELETE /works/<id>/associations/<type>/<target_id>` — the type is
912
+ # a path segment because it is part of the edge's identity, so retracting
913
+ # the figure claim leaves a transcription claim between the same two Works
914
+ # standing. Idempotent: retracting an edge that was never asserted is a
915
+ # no-op.
916
+ #
917
+ # @param work_id [String] the asserting Work's ID.
918
+ # @param target_id [String] the associated Work to drop.
919
+ # @param type [String] the predicate to retract, one of {ASSOCIATION_TYPES}.
920
+ # @param nuid [String, nil] optional acting user's NUID. On the relay-signing
921
+ # path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
922
+ # path it is ignored (identity lives in the token).
923
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
924
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
925
+ # omitted.
926
+ # @return [Hash] the Work's remaining associations, in the {.associations}
927
+ # shape.
928
+ # @raise [AtlasRb::WorkAssociationError] if Atlas rejects the write (HTTP
929
+ # 422) — an unresolvable target is the usual cause here.
930
+ # @raise [AtlasRb::ForbiddenError] if Atlas refuses the write (HTTP 403).
931
+ # @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no
932
+ # such Work, so the write did not happen.
933
+ # @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's
934
+ # status and body.
935
+ #
936
+ # @example
937
+ # AtlasRb::Work.disassociate("w-789", "w-123", type: "is_codebook_for")
938
+ # # => {"outbound" => {}, "inbound" => {}}
939
+ def self.disassociate(work_id, target_id, type:, nuid: nil, on_behalf_of: nil)
940
+ write_resource(
941
+ connection({}, nuid, on_behalf_of: on_behalf_of)
942
+ .delete(ROUTE + work_id + '/associations/' + type.to_s + '/' + target_id)
943
+ )
944
+ end
802
945
  end
803
946
  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.10.1
4
+ version: 1.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cliff
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-06 00:00:00.000000000 Z
11
+ date: 2026-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday