atlas_rb 1.10.0 → 1.11.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 +98 -0
- data/Gemfile.lock +1 -1
- data/README.md +22 -0
- data/lib/atlas_rb/admin/collection.rb +10 -2
- data/lib/atlas_rb/admin/community.rb +10 -2
- data/lib/atlas_rb/admin/work.rb +9 -4
- data/lib/atlas_rb/blob.rb +6 -1
- data/lib/atlas_rb/errors.rb +32 -0
- data/lib/atlas_rb/file_set.rb +6 -1
- data/lib/atlas_rb/middleware/raise_on_resource_error.rb +13 -3
- data/lib/atlas_rb/work.rb +216 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58d8d5524e43dc1f14f172ea65bc1e78e55826a63c814f2ef517675d07732d08
|
|
4
|
+
data.tar.gz: 548d4cba38f7a2ef81f210612157ee36a56de6e15b5fcff367b4df1b73fda847
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 745c494de1bd2da2a0048e2d3d562961bed6e48058b30da25dcbcdc0f9b2b7f3159c2b831508ac972a968dc6f02ee2a7f0a5cf11b85e2319277d1f200adf51f4
|
|
7
|
+
data.tar.gz: fe90db7467cdae4726357c2ba6d0d939d50c4174f1a3aa43ee8fd91adea19923fa321f53804fb52ed8db39eab22b578dadc6e542f77d54d1e1177e1effab0c53
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.11.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,103 @@
|
|
|
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
|
+
|
|
61
|
+
## 1.10.1
|
|
62
|
+
|
|
63
|
+
### Added — `Work.mark_incomplete` / `Work.clear_incomplete`
|
|
64
|
+
|
|
65
|
+
A Work can now carry a second lifecycle state. `complete` says the deposit
|
|
66
|
+
finished; `incomplete` says something downstream of it gave up. Before this
|
|
67
|
+
there was no way to record that at all: every give-up handler wrote a
|
|
68
|
+
`Rails.logger.warn` and nothing else, so a deposit whose PDF rendition
|
|
69
|
+
exhausted its retries ended up with no rendition and no thumbnail, the
|
|
70
|
+
depositor was not told, and no surface listed it.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
AtlasRb::Work.mark_incomplete(work_id, reason: "pdf_rendition_gave_up")
|
|
74
|
+
AtlasRb::Work.clear_incomplete(work_id)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`reason` is a machine token, one per give-up handler. Atlas holds it as an
|
|
78
|
+
opaque string and does **not** validate it against a list, so the vocabulary
|
|
79
|
+
belongs to the caller and a new token needs no Atlas release. Map it to display
|
|
80
|
+
text at the point of use, with a fallback for a token the view has not been
|
|
81
|
+
taught.
|
|
82
|
+
|
|
83
|
+
The flag **never hides** the Work. A record with its file, title and metadata
|
|
84
|
+
but one missing derivative is degraded, not broken, and stays readable.
|
|
85
|
+
|
|
86
|
+
Both bindings return the updated Work. `mark_incomplete` is idempotent and the
|
|
87
|
+
last reason wins; `clear_incomplete` is idempotent too, and clears the flag and
|
|
88
|
+
the reason together. Call it when a later run of the same job succeeds, which
|
|
89
|
+
makes the state self-healing.
|
|
90
|
+
|
|
91
|
+
### Added — `incomplete:` filter on `Work.list`
|
|
92
|
+
|
|
93
|
+
`AtlasRb::Work.list(incomplete: true)` is the staff list of Works whose
|
|
94
|
+
pipeline gave up, the sibling of the existing `in_progress:` "what's stuck?"
|
|
95
|
+
view. The two are independent and combine: `in_progress: false, incomplete:
|
|
96
|
+
true` reads as "finished, but degraded".
|
|
97
|
+
|
|
98
|
+
Work summaries in the response now carry `incomplete` and `incomplete_reason`
|
|
99
|
+
alongside `in_progress`.
|
|
100
|
+
|
|
3
101
|
## 1.10.0
|
|
4
102
|
|
|
5
103
|
### Removed — `title` on `Person.create` and `Person.update`
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -255,6 +255,28 @@ AtlasRb::Work.list(in_progress: false, page: 2) # completed deposits, page 2
|
|
|
255
255
|
AtlasRb::Work.complete("w-789") # mark w-789 done
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
+
A Work carries a second, independent lifecycle state for the other kind of
|
|
259
|
+
failure. `complete` says the deposit finished; `incomplete` says something
|
|
260
|
+
downstream of it gave up — a rendition, the derivatives, the full-text
|
|
261
|
+
extraction — after that job exhausted its retries. Call `mark_incomplete`
|
|
262
|
+
from the give-up handler, and `clear_incomplete` when a later run succeeds,
|
|
263
|
+
which makes the state self-healing.
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
AtlasRb::Work.mark_incomplete("w-789", reason: "pdf_rendition_gave_up")
|
|
267
|
+
AtlasRb::Work.clear_incomplete("w-789")
|
|
268
|
+
AtlasRb::Work.list(incomplete: true) # the staff repair list
|
|
269
|
+
AtlasRb::Work.list(in_progress: false, incomplete: true) # finished, but degraded
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`reason` is a machine token, one per give-up handler. Atlas stores it as an
|
|
273
|
+
opaque string and does not validate it against a list, so the vocabulary is
|
|
274
|
+
yours and a new token needs no Atlas release. Map it to display text at the
|
|
275
|
+
point of use, with a fallback for a token the view has not been taught.
|
|
276
|
+
|
|
277
|
+
The flag never hides the Work. A record with its file, title and metadata but
|
|
278
|
+
one missing derivative is degraded, not broken, and stays readable.
|
|
279
|
+
|
|
258
280
|
### Audit-event history
|
|
259
281
|
|
|
260
282
|
`Resource.history` wraps Atlas's `GET /resources/<id>/history` endpoint
|
|
@@ -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.
|
|
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.
|
|
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
|
data/lib/atlas_rb/admin/work.rb
CHANGED
|
@@ -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
|
|
19
|
-
#
|
|
20
|
-
#
|
|
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
|
|
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
|
data/lib/atlas_rb/errors.rb
CHANGED
|
@@ -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
|
data/lib/atlas_rb/file_set.rb
CHANGED
|
@@ -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`)
|
|
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
|
@@ -43,6 +43,11 @@ module AtlasRb
|
|
|
43
43
|
#
|
|
44
44
|
# @param in_progress [Boolean, nil] when set, filter to Works whose
|
|
45
45
|
# `in_progress` flag matches. Omit (or pass `nil`) for "all works".
|
|
46
|
+
# @param incomplete [Boolean, nil] when set, filter to Works whose
|
|
47
|
+
# `incomplete` flag matches — the staff list of Works whose enrichment
|
|
48
|
+
# pipeline gave up (see {.mark_incomplete}). Independent of
|
|
49
|
+
# `in_progress:`, and the two combine: `in_progress: false,
|
|
50
|
+
# incomplete: true` reads as "finished, but degraded".
|
|
46
51
|
# @param page [Integer, nil] 1-indexed page number.
|
|
47
52
|
# @param per_page [Integer, nil] page size override.
|
|
48
53
|
# @param nuid [String, nil] optional acting user's NUID. On the relay-signing
|
|
@@ -53,16 +58,20 @@ module AtlasRb
|
|
|
53
58
|
# omitted.
|
|
54
59
|
# @return [AtlasRb::Mash] `{ "works" => [...], "pagination" => {...} }`.
|
|
55
60
|
# Each entry in `"works"` is a Work summary (`id`, `title`,
|
|
56
|
-
# `description`, `in_progress`).
|
|
61
|
+
# `description`, `in_progress`, `incomplete`, `incomplete_reason`).
|
|
57
62
|
#
|
|
58
63
|
# @example Find stuck deposits
|
|
59
64
|
# AtlasRb::Work.list(in_progress: true)
|
|
60
65
|
#
|
|
66
|
+
# @example Find works whose pipeline gave up
|
|
67
|
+
# AtlasRb::Work.list(incomplete: true)
|
|
68
|
+
#
|
|
61
69
|
# @example Page through all works
|
|
62
70
|
# AtlasRb::Work.list(page: 2, per_page: 50)
|
|
63
|
-
def self.list(in_progress: nil, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil)
|
|
71
|
+
def self.list(in_progress: nil, incomplete: nil, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil)
|
|
64
72
|
params = {}
|
|
65
73
|
params[:in_progress] = in_progress unless in_progress.nil?
|
|
74
|
+
params[:incomplete] = incomplete unless incomplete.nil?
|
|
66
75
|
params[:page] = page if page
|
|
67
76
|
params[:per_page] = per_page if per_page
|
|
68
77
|
AtlasRb::Mash.new(JSON.parse(
|
|
@@ -232,6 +241,88 @@ module AtlasRb
|
|
|
232
241
|
connection({}, nuid, on_behalf_of: on_behalf_of).post(ROUTE + id + '/complete')
|
|
233
242
|
end
|
|
234
243
|
|
|
244
|
+
# Flag a Work whose enrichment pipeline gave up.
|
|
245
|
+
#
|
|
246
|
+
# The counterpart to {.complete}, for the other half of the lifecycle:
|
|
247
|
+
# `complete` says the deposit finished, this says something downstream of
|
|
248
|
+
# it did not. Call it from a work-scoped job's give-up handler — the PDF or
|
|
249
|
+
# media rendition, the derivatives, the full-text extraction — once that
|
|
250
|
+
# job has exhausted its retries. Atlas's `GET /works?incomplete=true` then
|
|
251
|
+
# lists the Work for staff, and `incomplete_bsi` on its Solr document lets
|
|
252
|
+
# a result row render a pill without a per-row fetch.
|
|
253
|
+
#
|
|
254
|
+
# The flag **never hides** the Work. A record with its file, title and
|
|
255
|
+
# metadata but one missing derivative is degraded, not broken, and stays
|
|
256
|
+
# readable — enrichment does not fail a deposit.
|
|
257
|
+
#
|
|
258
|
+
# Idempotent on the server; the last reason wins. Clear it with
|
|
259
|
+
# {.clear_incomplete} when a later run of the same job succeeds, which
|
|
260
|
+
# makes the state self-healing.
|
|
261
|
+
#
|
|
262
|
+
# @param id [String] the Work ID.
|
|
263
|
+
# @param reason [String, nil] a machine token naming the cause — one per
|
|
264
|
+
# give-up handler, e.g. `"pdf_rendition_gave_up"`,
|
|
265
|
+
# `"media_rendition_gave_up"`, `"ingest_gave_up"`. Atlas stores it as an
|
|
266
|
+
# opaque string and does **not** validate it against a list, so the
|
|
267
|
+
# vocabulary is the caller's and a new token needs no Atlas release. Map
|
|
268
|
+
# it to display text at the point of use, with a fallback for a token the
|
|
269
|
+
# view has not been taught. A blank reason still sets the flag.
|
|
270
|
+
# @param nuid [String, nil] optional NUID of the acting user.
|
|
271
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
272
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
273
|
+
# omitted.
|
|
274
|
+
# @return [Hash] the updated Work, the same shape {.find} returns, carrying
|
|
275
|
+
# `incomplete` and `incomplete_reason`.
|
|
276
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
277
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
278
|
+
# `error: "stale_resource"`).
|
|
279
|
+
# @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
|
|
280
|
+
# resource, so the write did not happen.
|
|
281
|
+
# @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
|
|
282
|
+
# and body.
|
|
283
|
+
#
|
|
284
|
+
# @example In a give-up handler
|
|
285
|
+
# AtlasRb::Work.mark_incomplete(work_id, reason: "pdf_rendition_gave_up")
|
|
286
|
+
def self.mark_incomplete(id, reason:, nuid: nil, on_behalf_of: nil)
|
|
287
|
+
AtlasRb::Mash.new(write_resource(
|
|
288
|
+
connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
289
|
+
.post(ROUTE + id + '/incomplete', JSON.dump(reason: reason))
|
|
290
|
+
))["work"]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Clear the incomplete flag and its reason.
|
|
294
|
+
#
|
|
295
|
+
# The repair half of {.mark_incomplete}: call it from the same job when a
|
|
296
|
+
# later run succeeds, or by hand once an operator has fixed the Work. Both
|
|
297
|
+
# fields clear together — a reason without a flag would leave a stale cause
|
|
298
|
+
# on the Solr document.
|
|
299
|
+
#
|
|
300
|
+
# Idempotent: clearing a Work that was never flagged is a no-op.
|
|
301
|
+
#
|
|
302
|
+
# @param id [String] the Work ID.
|
|
303
|
+
# @param nuid [String, nil] optional NUID of the acting user.
|
|
304
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
305
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
306
|
+
# omitted.
|
|
307
|
+
# @return [Hash] the updated Work, the same shape {.find} returns, with
|
|
308
|
+
# `incomplete` false and `incomplete_reason` null.
|
|
309
|
+
# @raise [AtlasRb::StaleResourceError] if Atlas reports an optimistic-lock
|
|
310
|
+
# conflict that exhausted its internal retry budget (HTTP 409 with
|
|
311
|
+
# `error: "stale_resource"`).
|
|
312
|
+
# @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no such
|
|
313
|
+
# resource, so the write did not happen.
|
|
314
|
+
# @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's status
|
|
315
|
+
# and body.
|
|
316
|
+
#
|
|
317
|
+
# @example On a later successful run
|
|
318
|
+
# AtlasRb::Work.clear_incomplete(work_id)
|
|
319
|
+
def self.clear_incomplete(id, nuid: nil, on_behalf_of: nil)
|
|
320
|
+
AtlasRb::Mash.new(write_resource(
|
|
321
|
+
connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
322
|
+
.delete(ROUTE + id + '/incomplete')
|
|
323
|
+
))["work"]
|
|
324
|
+
end
|
|
325
|
+
|
|
235
326
|
# Replace a Work's metadata by uploading a MODS XML document.
|
|
236
327
|
#
|
|
237
328
|
# @param id [String] the Work ID.
|
|
@@ -708,5 +799,128 @@ module AtlasRb
|
|
|
708
799
|
.delete(ROUTE + work_id + '/linked_members/' + collection_id)
|
|
709
800
|
)
|
|
710
801
|
end
|
|
802
|
+
|
|
803
|
+
# The five relationship predicates Atlas accepts, carried here so a caller
|
|
804
|
+
# can build a select box without hard-coding the vocabulary. Adding a sixth
|
|
805
|
+
# needs an Atlas release, so this list cannot drift ahead of the server.
|
|
806
|
+
ASSOCIATION_TYPES = %w[
|
|
807
|
+
is_codebook_for
|
|
808
|
+
is_figure_for
|
|
809
|
+
is_instructional_material_for
|
|
810
|
+
is_supplemental_material_for
|
|
811
|
+
is_transcription_of
|
|
812
|
+
].freeze
|
|
813
|
+
|
|
814
|
+
# List a Work's typed associations with other Works.
|
|
815
|
+
#
|
|
816
|
+
# Wraps `GET /works/<id>/associations`. An association is DRS v1's
|
|
817
|
+
# "associated works": a directed claim that one object is the codebook,
|
|
818
|
+
# figure, transcription, instructional material or supplemental material
|
|
819
|
+
# **for** another. Both objects stay separate records — this is not
|
|
820
|
+
# membership, and nothing moves in the containment tree.
|
|
821
|
+
#
|
|
822
|
+
# The edge is stored once, on the Work that asserts it. Atlas derives the
|
|
823
|
+
# other direction, so `outbound` and `inbound` can never disagree.
|
|
824
|
+
#
|
|
825
|
+
# @param id [String] the Work ID.
|
|
826
|
+
# @param nuid [String, nil] optional acting user's NUID. On the relay-signing
|
|
827
|
+
# path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
|
|
828
|
+
# path it is ignored (identity lives in the token).
|
|
829
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
830
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
831
|
+
# omitted.
|
|
832
|
+
# @return [Hash] `{"outbound" => {predicate => [noid, …]}, "inbound" => {…}}`.
|
|
833
|
+
# `outbound` is what this Work asserts, `inbound` what other Works assert
|
|
834
|
+
# about it. Predicates holding no edges are omitted, so both maps are
|
|
835
|
+
# `{}` for an unassociated Work.
|
|
836
|
+
#
|
|
837
|
+
# @example
|
|
838
|
+
# AtlasRb::Work.associations("w-789")
|
|
839
|
+
# # => {"outbound" => {"is_codebook_for" => ["w-123"]}, "inbound" => {}}
|
|
840
|
+
def self.associations(id, nuid: nil, on_behalf_of: nil)
|
|
841
|
+
JSON.parse(
|
|
842
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/associations')&.body
|
|
843
|
+
)
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# Assert that this Work stands in a typed relationship to another Work.
|
|
847
|
+
#
|
|
848
|
+
# Wraps `POST /works/<id>/associations` with a `work_id` + `type` body.
|
|
849
|
+
# The edge is stored on **this** Work only; `target` reports the same edge
|
|
850
|
+
# under `inbound`. Asserting an edge that already exists is a no-op, and
|
|
851
|
+
# two Works can hold several different edges at once.
|
|
852
|
+
#
|
|
853
|
+
# A cycle is permitted and meaningful — "A is a transcription of B" and
|
|
854
|
+
# "B is a figure for A" can both be true.
|
|
855
|
+
#
|
|
856
|
+
# @param work_id [String] the asserting Work's ID (the codebook, figure, …).
|
|
857
|
+
# @param target_id [String] the Work being pointed at (the dataset, article, …).
|
|
858
|
+
# @param type [String] one of {ASSOCIATION_TYPES}.
|
|
859
|
+
# @param nuid [String, nil] optional acting user's NUID. On the relay-signing
|
|
860
|
+
# path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
|
|
861
|
+
# path it is ignored (identity lives in the token).
|
|
862
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
863
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
864
|
+
# omitted.
|
|
865
|
+
# @return [Hash] the Work's associations *after* the add, in the
|
|
866
|
+
# {.associations} shape — so no follow-up GET is needed.
|
|
867
|
+
# @raise [AtlasRb::WorkAssociationError] if Atlas rejects the claim (HTTP
|
|
868
|
+
# 422): unknown type, unresolvable target, a non-Work target, the Work
|
|
869
|
+
# itself, or either end tombstoned. The envelope's `error` code is
|
|
870
|
+
# exposed as `#code`.
|
|
871
|
+
# @raise [AtlasRb::ForbiddenError] if Atlas refuses the write (HTTP 403).
|
|
872
|
+
# Associating is admin / devolved-admin only, because the claim renders
|
|
873
|
+
# on the target's page too.
|
|
874
|
+
# @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no
|
|
875
|
+
# such Work, so the write did not happen.
|
|
876
|
+
# @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's
|
|
877
|
+
# status and body.
|
|
878
|
+
#
|
|
879
|
+
# @example
|
|
880
|
+
# AtlasRb::Work.associate("w-789", "w-123", type: "is_codebook_for")
|
|
881
|
+
# # => {"outbound" => {"is_codebook_for" => ["w-123"]}, "inbound" => {}}
|
|
882
|
+
def self.associate(work_id, target_id, type:, nuid: nil, on_behalf_of: nil)
|
|
883
|
+
write_resource(
|
|
884
|
+
connection({ work_id: target_id, type: type }, nuid, on_behalf_of: on_behalf_of)
|
|
885
|
+
.post(ROUTE + work_id + '/associations')
|
|
886
|
+
)
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
# Retract one typed relationship between two Works.
|
|
890
|
+
#
|
|
891
|
+
# Wraps `DELETE /works/<id>/associations/<type>/<target_id>` — the type is
|
|
892
|
+
# a path segment because it is part of the edge's identity, so retracting
|
|
893
|
+
# the figure claim leaves a transcription claim between the same two Works
|
|
894
|
+
# standing. Idempotent: retracting an edge that was never asserted is a
|
|
895
|
+
# no-op.
|
|
896
|
+
#
|
|
897
|
+
# @param work_id [String] the asserting Work's ID.
|
|
898
|
+
# @param target_id [String] the associated Work to drop.
|
|
899
|
+
# @param type [String] the predicate to retract, one of {ASSOCIATION_TYPES}.
|
|
900
|
+
# @param nuid [String, nil] optional acting user's NUID. On the relay-signing
|
|
901
|
+
# path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
|
|
902
|
+
# path it is ignored (identity lives in the token).
|
|
903
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
904
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
905
|
+
# omitted.
|
|
906
|
+
# @return [Hash] the Work's remaining associations, in the {.associations}
|
|
907
|
+
# shape.
|
|
908
|
+
# @raise [AtlasRb::WorkAssociationError] if Atlas rejects the write (HTTP
|
|
909
|
+
# 422) — an unresolvable target is the usual cause here.
|
|
910
|
+
# @raise [AtlasRb::ForbiddenError] if Atlas refuses the write (HTTP 403).
|
|
911
|
+
# @raise [AtlasRb::NotFoundError] if Atlas answers `404` — the id names no
|
|
912
|
+
# such Work, so the write did not happen.
|
|
913
|
+
# @raise [AtlasRb::ResourceError] on any other non-2xx, carrying Atlas's
|
|
914
|
+
# status and body.
|
|
915
|
+
#
|
|
916
|
+
# @example
|
|
917
|
+
# AtlasRb::Work.disassociate("w-789", "w-123", type: "is_codebook_for")
|
|
918
|
+
# # => {"outbound" => {}, "inbound" => {}}
|
|
919
|
+
def self.disassociate(work_id, target_id, type:, nuid: nil, on_behalf_of: nil)
|
|
920
|
+
write_resource(
|
|
921
|
+
connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
922
|
+
.delete(ROUTE + work_id + '/associations/' + type.to_s + '/' + target_id)
|
|
923
|
+
)
|
|
924
|
+
end
|
|
711
925
|
end
|
|
712
926
|
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.
|
|
4
|
+
version: 1.11.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-08-
|
|
11
|
+
date: 2026-08-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|