valkyrie 1.1.2 → 1.2.0.rc1
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 +5 -5
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +5 -0
- data/CHANGELOG.md +28 -0
- data/README.md +43 -7
- data/db/migrate/20180802220739_add_optimistic_locking_to_orm_resources.rb +6 -0
- data/lib/generators/valkyrie/templates/resource.rb.erb +0 -1
- data/lib/valkyrie/change_set.rb +4 -0
- data/lib/valkyrie/id.rb +5 -0
- data/lib/valkyrie/metadata_adapter.rb +1 -0
- data/lib/valkyrie/persistence/composite_persister.rb +12 -1
- data/lib/valkyrie/persistence/fedora/list_node.rb +3 -3
- data/lib/valkyrie/persistence/fedora/metadata_adapter.rb +4 -0
- data/lib/valkyrie/persistence/fedora/permissive_schema.rb +5 -0
- data/lib/valkyrie/persistence/fedora/persister/alternate_identifier.rb +0 -1
- data/lib/valkyrie/persistence/fedora/persister/model_converter.rb +2 -0
- data/lib/valkyrie/persistence/fedora/persister/orm_converter.rb +33 -3
- data/lib/valkyrie/persistence/fedora/persister.rb +57 -16
- data/lib/valkyrie/persistence/fedora/query_service.rb +2 -0
- data/lib/valkyrie/persistence/fedora.rb +2 -0
- data/lib/valkyrie/persistence/memory/metadata_adapter.rb +4 -0
- data/lib/valkyrie/persistence/memory/persister.rb +37 -14
- data/lib/valkyrie/persistence/memory/query_service.rb +2 -0
- data/lib/valkyrie/persistence/optimistic_lock_token.rb +34 -0
- data/lib/valkyrie/persistence/postgres/metadata_adapter.rb +11 -2
- data/lib/valkyrie/persistence/postgres/orm_converter.rb +35 -5
- data/lib/valkyrie/persistence/postgres/persister.rb +10 -12
- data/lib/valkyrie/persistence/postgres/query_service.rb +5 -4
- data/lib/valkyrie/persistence/postgres/resource_converter.rb +21 -1
- data/lib/valkyrie/persistence/postgres/resource_factory.rb +22 -18
- data/lib/valkyrie/persistence/solr/metadata_adapter.rb +5 -1
- data/lib/valkyrie/persistence/solr/model_converter.rb +32 -2
- data/lib/valkyrie/persistence/solr/orm_converter.rb +22 -5
- data/lib/valkyrie/persistence/solr/persister.rb +2 -0
- data/lib/valkyrie/persistence/solr/query_service.rb +2 -0
- data/lib/valkyrie/persistence/solr/repository.rb +21 -8
- data/lib/valkyrie/persistence/solr/resource_factory.rb +6 -3
- data/lib/valkyrie/persistence.rb +10 -3
- data/lib/valkyrie/resource/access_controls.rb +0 -1
- data/lib/valkyrie/resource.rb +31 -8
- data/lib/valkyrie/specs/shared_specs/change_set_persister.rb +0 -1
- data/lib/valkyrie/specs/shared_specs/metadata_adapter.rb +9 -0
- data/lib/valkyrie/specs/shared_specs/persister.rb +134 -7
- data/lib/valkyrie/specs/shared_specs/queries.rb +20 -2
- data/lib/valkyrie/specs/shared_specs/resource.rb +0 -1
- data/lib/valkyrie/specs/shared_specs/storage_adapter.rb +0 -1
- data/lib/valkyrie/storage/fedora.rb +29 -20
- data/lib/valkyrie/types.rb +11 -1
- data/lib/valkyrie/version.rb +1 -1
- data/lib/valkyrie.rb +0 -2
- metadata +7 -6
- data/config/fedora.yml +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8a320a108f82f79f88b27eaeb267b7b134499e346dccaae4f93165ff95e47fa5
|
4
|
+
data.tar.gz: bebc13ba65b1e1e2d69fd9a9c9408f8af8ef4484984a6de568db4c90c34eb15f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8a32748fc47406abdb640ce4191e6271c61a0813cb2774a444ed5c6a45a5a34fd8e599f37afab22ac6c76d71d25bcaee28df6801a3c57add7617b064fc5ed3fb
|
7
|
+
data.tar.gz: b353cc383371599df898f626b6e211294a34f5e351dad0fc0521a11841e79c04da3699c1e867f54e102c09379cba0366fa62e408738c3f7fa0f2637a20d59a14
|
data/.rubocop.yml
CHANGED
data/.rubocop_todo.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,31 @@
|
|
1
|
+
# v1.2.0.RC1 2018-08-09
|
2
|
+
|
3
|
+
## Changes since last release
|
4
|
+
|
5
|
+
* Support for single values.
|
6
|
+
[Documentation](https://github.com/samvera-labs/valkyrie/wiki/Using-Types#singular-values)
|
7
|
+
* Optimistic Locking.
|
8
|
+
[Documentation](https://github.com/samvera-labs/valkyrie/wiki/Optimistic-Locking)
|
9
|
+
* Remove reliance on ActiveFedora for Fedora Storage Adapter.
|
10
|
+
* Only load adapters if referenced.
|
11
|
+
* Postgres Adapter uses transactions for `save_all`
|
12
|
+
* Resources now include `id` attribute by default.
|
13
|
+
|
14
|
+
## Special Thanks
|
15
|
+
|
16
|
+
This release was made possible by a community sprint undertaken between Penn
|
17
|
+
State University Libraries & Princeton University Library. Thanks to the
|
18
|
+
following participants who made it happen:
|
19
|
+
|
20
|
+
* [awead](https://github.com/awead)
|
21
|
+
* [cam156](https://github.com/cam156)
|
22
|
+
* [DanCoughlin](https://github.com/DanCoughlin)
|
23
|
+
* [escowles](https://github.com/escowles)
|
24
|
+
* [hackmastera](https://github.com/hackmastera)
|
25
|
+
* [jrgriffiniii](https://github.com/jrgriffiniii)
|
26
|
+
* [mtribone](https://github.com/mtribone)
|
27
|
+
* [tpendragon](https://github.com/tpendragon)
|
28
|
+
|
1
29
|
# v1.1.2 2018-06-08
|
2
30
|
|
3
31
|
## Changes since last release
|
data/README.md
CHANGED
@@ -5,21 +5,30 @@ Valkyrie is a gem for enabling multiple backends for storage of files and metada
|
|
5
5
|
[](https://circleci.com/gh/samvera-labs/valkyrie)
|
6
6
|
[](https://coveralls.io/github/samvera-labs/valkyrie?branch=master)
|
7
7
|
[](https://waffle.io/samvera-labs/valkyrie)
|
8
|
+
[](http://rubydoc.info/github/samvera-labs/valkyrie)
|
8
9
|
|
10
|
+
## Primary Contacts
|
11
|
+
|
12
|
+
### Product Owner
|
13
|
+
|
14
|
+
[Carolyn Cole](https://github.com/cam156)
|
15
|
+
|
16
|
+
### Technical Lead
|
17
|
+
|
18
|
+
[Trey Pendragon](https://github.com/tpendragon)
|
9
19
|
|
10
20
|
## Installation
|
11
21
|
|
12
22
|
Add this line to your application's Gemfile:
|
13
23
|
|
14
|
-
```
|
15
|
-
gem 'valkyrie'
|
24
|
+
```
|
25
|
+
gem 'valkyrie'
|
16
26
|
```
|
17
27
|
|
18
28
|
And then execute:
|
19
29
|
|
20
30
|
$ bundle
|
21
31
|
|
22
|
-
|
23
32
|
## Configuration
|
24
33
|
|
25
34
|
Valkyrie is configured in two places: an initializer that registers the persistence options and a YAML
|
@@ -30,6 +39,7 @@ configuration file that sets which options are used by default in which environm
|
|
30
39
|
Here is a sample initializer that registers a couple adapters and storage adapters, in each case linking an
|
31
40
|
instance with a short name that can be used to refer to it in your application:
|
32
41
|
|
42
|
+
|
33
43
|
```
|
34
44
|
# frozen_string_literal: true
|
35
45
|
require 'valkyrie'
|
@@ -50,7 +60,7 @@ Rails.application.config.to_prepare do
|
|
50
60
|
)
|
51
61
|
|
52
62
|
Valkyrie::StorageAdapter.register(
|
53
|
-
Valkyrie::Storage::Fedora.new(connection:
|
63
|
+
Valkyrie::Storage::Fedora.new(connection: Ldp::Client.new("http://localhost:8988/rest")),
|
54
64
|
:fedora
|
55
65
|
)
|
56
66
|
|
@@ -69,7 +79,7 @@ The initializer registers two `Valkyrie::MetadataAdapter` instances for storing
|
|
69
79
|
|
70
80
|
Other adapter options include `Valkyrie::Persistence::BufferedPersister` for buffering in memory before bulk
|
71
81
|
updating another persister, `Valkyrie::Persistence::CompositePersister` for storing in more than one adapter
|
72
|
-
at once, and `Valkyrie::Persistence::
|
82
|
+
at once, `Valkyrie::Persistence::Solr` for storing in Solr, and `Valkyrie::Persistence::Fedora` for storing in Fedora.
|
73
83
|
|
74
84
|
The initializer also registers three `Valkyrie::StorageAdapter` instances for storing files:
|
75
85
|
* `:disk` which stores files on disk
|
@@ -101,9 +111,23 @@ For each environment, you must set two values:
|
|
101
111
|
|
102
112
|
The values are the short names used in your initializer.
|
103
113
|
|
114
|
+
Further details can be found on the [the Wiki](https://github.com/samvera-labs/valkyrie/wiki/Persistence).
|
104
115
|
|
105
116
|
## Usage
|
106
117
|
|
118
|
+
### The Public API
|
119
|
+
|
120
|
+
Valkyrie's public API is defined by the shared specs that are used to test each of its core classes.
|
121
|
+
This include change sets, resources, persisters, adapters, and queries. When creating your own kinds of
|
122
|
+
these kinds of classes, you should use these shared specs to test your classes for conformance to
|
123
|
+
Valkyrie's API.
|
124
|
+
|
125
|
+
When breaking changes are introduced, necessitating a major version change, the shared specs will reflect
|
126
|
+
this. Likewise, non-breaking changes to Valkyrie can be defined as code changes that do not cause any
|
127
|
+
errors with the current shared specs.
|
128
|
+
|
129
|
+
Using the shared specs in your own models is described in more [detail](https://github.com/samvera-labs/valkyrie/wiki/Shared-Specs).
|
130
|
+
|
107
131
|
### Define a Custom Work
|
108
132
|
|
109
133
|
Define a custom work class:
|
@@ -112,12 +136,13 @@ Define a custom work class:
|
|
112
136
|
# frozen_string_literal: true
|
113
137
|
class MyModel < Valkyrie::Resource
|
114
138
|
include Valkyrie::Resource::AccessControls
|
115
|
-
attribute :id, Valkyrie::Types::ID.optional # Optional to allow auto-generation of IDs
|
116
139
|
attribute :title, Valkyrie::Types::Set # Sets are unordered
|
117
140
|
attribute :authors, Valkyrie::Types::Array # Arrays are ordered
|
118
141
|
end
|
119
142
|
```
|
120
143
|
|
144
|
+
Defining resource attributes is explained in greater detail within the [Wiki](https://github.com/samvera-labs/valkyrie/wiki/Using-Types).
|
145
|
+
|
121
146
|
#### Work Types Generator
|
122
147
|
|
123
148
|
To create a custom Valkyrie model in your application, you can use the Rails generator. For example, to
|
@@ -153,6 +178,10 @@ objects = adapter.query_service.find_all
|
|
153
178
|
Valkyrie.config.metadata_adapter.query_service.find_all_of_model(model: MyModel)
|
154
179
|
```
|
155
180
|
|
181
|
+
The usage of `ChangeSets` in writing data are further documented [here](https://github.com/samvera-labs/valkyrie/wiki/ChangeSets-and-Dirty-Tracking).
|
182
|
+
|
183
|
+
### Concurrency Support (Optimistic Locking)
|
184
|
+
By default, it is assumed that a Valkyrie repository implementation shall use a solution supporting concurrent updates for resources (multiple resources can be updated simultaneously using a Gem such as [Sidekiq](https://github.com/mperham/sidekiq)). In order to handle the possibility of multiple updates applied to the same resource corrupting data, Valkyrie supports optimistic locking. For further details, please reference the [overview of optimistic locking for Valkyrie resources](https://github.com/samvera-labs/valkyrie/wiki/Optimistic-Locking).
|
156
185
|
|
157
186
|
## Installing a Development environment
|
158
187
|
|
@@ -183,7 +212,7 @@ Valkyrie.config.metadata_adapter.query_service.find_all_of_model(model: MyModel)
|
|
183
212
|
|
184
213
|
1. `docker-machine create default`
|
185
214
|
1. `docker-machine start default`
|
186
|
-
1. `eval "$(docker-machine env)"
|
215
|
+
1. `eval "$(docker-machine env)"`
|
187
216
|
|
188
217
|
#### Starting the development mode dependencies
|
189
218
|
1. Start Solr, Fedora, and PostgreSQL with `rake docker:dev:daemon` (or `rake docker:dev:up` in a separate shell to run them in the foreground)
|
@@ -204,6 +233,13 @@ Valkyrie.config.metadata_adapter.query_service.find_all_of_model(model: MyModel)
|
|
204
233
|
|
205
234
|
The development and test stacks use fully contained virtual volumes and bind all services to different ports, so they can be running at the same time without issue.
|
206
235
|
|
236
|
+
## Get Help
|
237
|
+
|
238
|
+
If you have any questions regarding Valkyrie you can send a message to [the
|
239
|
+
Samvera community tech list](mailto:samvera-tech@googlegroups.com) or the `#valkyrie`
|
240
|
+
channel in the [Samvera community Slack
|
241
|
+
team](https://wiki.duraspace.org/pages/viewpage.action?pageId=87460391#Getintouch!-Slack).
|
242
|
+
|
207
243
|
## License
|
208
244
|
|
209
245
|
Valkyrie is available under [the Apache 2.0 license](../LICENSE).
|
@@ -2,7 +2,6 @@
|
|
2
2
|
# Generated with `rails generate valkyrie:model <%= class_name %>`
|
3
3
|
class <%= class_name %> < Valkyrie::Resource
|
4
4
|
include Valkyrie::Resource::AccessControls
|
5
|
-
attribute :id, Valkyrie::Types::ID.optional
|
6
5
|
<%- attributes.each do |att| -%>
|
7
6
|
attribute :<%= att.name %>, Valkyrie::Types::<%= (att.type == :array) ? 'Array' : 'Set' %>
|
8
7
|
<%- end -%>
|
data/lib/valkyrie/change_set.rb
CHANGED
@@ -24,12 +24,15 @@ module Valkyrie
|
|
24
24
|
property :append_id, virtual: true
|
25
25
|
|
26
26
|
# Set ID of record this one should be appended to.
|
27
|
+
# We use append_id to add a member/child onto an existing list of members.
|
27
28
|
# @param append_id [Valkyrie::ID]
|
28
29
|
def append_id=(append_id)
|
29
30
|
super(Valkyrie::ID.new(append_id))
|
30
31
|
end
|
31
32
|
|
32
33
|
# Returns whether or not a given field has multiple values.
|
34
|
+
# Multiple values are useful for fields like creator, author, title, etc.
|
35
|
+
# where there may be more than one value for a field that is stored and returned in the UI
|
33
36
|
# @param field_name [Symbol]
|
34
37
|
# @return [Boolean]
|
35
38
|
def multiple?(field_name)
|
@@ -37,6 +40,7 @@ module Valkyrie
|
|
37
40
|
end
|
38
41
|
|
39
42
|
# Returns whether or not a given field is required.
|
43
|
+
# Useful for distinguishing required fields in a form and for validation
|
40
44
|
# @param field_name [Symbol]
|
41
45
|
# @return [Boolean]
|
42
46
|
def required?(field_name)
|
data/lib/valkyrie/id.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Valkyrie
|
3
3
|
# A simple ID class to keep IDs distinguished from strings
|
4
|
+
# In order for an object to be queryable via joins, it needs
|
5
|
+
# to be added as a reference via a Valkyrie::ID rather than just a string ID.
|
4
6
|
class ID
|
5
7
|
attr_reader :id
|
6
8
|
delegate :empty?, to: :id
|
@@ -19,8 +21,11 @@ module Valkyrie
|
|
19
21
|
end
|
20
22
|
alias == eql?
|
21
23
|
|
24
|
+
# @deprecated Please use {.uri_for} instead
|
22
25
|
def to_uri
|
23
26
|
return RDF::Literal.new(id.to_s, datatype: RDF::URI("http://example.com/valkyrie_id")) if id.to_s.include?("://")
|
27
|
+
warn "[DEPRECATION] `to_uri` is deprecated and will be removed in the next major release. " \
|
28
|
+
"Called from #{Gem.location_of_caller.join(':')}"
|
24
29
|
::RDF::URI(ActiveFedora::Base.id_to_uri(id))
|
25
30
|
end
|
26
31
|
|
@@ -8,6 +8,7 @@ module Valkyrie
|
|
8
8
|
self.adapters = {}
|
9
9
|
class << self
|
10
10
|
# Register an adapter by a short name.
|
11
|
+
# Registering an adapter by a short name makes the adapter easier to find and reference.
|
11
12
|
# @param adapter [#persister,#query_service] Adapter to register.
|
12
13
|
# @param short_name [Symbol] Name to register it under.
|
13
14
|
def register(adapter, short_name)
|
@@ -19,7 +19,15 @@ module Valkyrie::Persistence
|
|
19
19
|
|
20
20
|
# (see Valkyrie::Persistence::Memory::Persister#save)
|
21
21
|
def save(resource:)
|
22
|
-
|
22
|
+
# Assume the first persister is the canonical data store; that's the optlock we want
|
23
|
+
first, *rest = *persisters
|
24
|
+
cached_resource = first.save(resource: resource)
|
25
|
+
# Don't pass opt lock tokens to other persisters
|
26
|
+
internal_resource = cached_resource.dup
|
27
|
+
internal_resource.send("#{Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK}=", []) if internal_resource.optimistic_locking_enabled?
|
28
|
+
rest.inject(internal_resource) { |m, persister| persister.save(resource: m) }
|
29
|
+
# return the one with the desired opt lock token
|
30
|
+
cached_resource
|
23
31
|
end
|
24
32
|
|
25
33
|
# (see Valkyrie::Persistence::Memory::Persister#save_all)
|
@@ -27,6 +35,9 @@ module Valkyrie::Persistence
|
|
27
35
|
resources.map do |resource|
|
28
36
|
save(resource: resource)
|
29
37
|
end
|
38
|
+
rescue Valkyrie::Persistence::StaleObjectError
|
39
|
+
# clear out any IDs returned to reduce potential confusion
|
40
|
+
raise Valkyrie::Persistence::StaleObjectError
|
30
41
|
end
|
31
42
|
|
32
43
|
# (see Valkyrie::Persistence::Memory::Persister#delete)
|
@@ -18,7 +18,7 @@ module Valkyrie::Persistence::Fedora
|
|
18
18
|
end
|
19
19
|
|
20
20
|
# Returns the next proxy or a tail sentinel.
|
21
|
-
# @return [
|
21
|
+
# @return [RDF::URI]
|
22
22
|
def next
|
23
23
|
@next ||=
|
24
24
|
if next_uri
|
@@ -31,13 +31,13 @@ module Valkyrie::Persistence::Fedora
|
|
31
31
|
end
|
32
32
|
|
33
33
|
# Returns the previous proxy or a head sentinel.
|
34
|
-
# @return [
|
34
|
+
# @return [RDF::URI]
|
35
35
|
def prev
|
36
36
|
@prev ||= node_cache.fetch(prev_uri) if prev_uri
|
37
37
|
end
|
38
38
|
|
39
39
|
# Graph representation of node.
|
40
|
-
# @return [
|
40
|
+
# @return [Valkyrie::Persistence::Fedora::ListNode::Resource]
|
41
41
|
def to_graph
|
42
42
|
return RDF::Graph.new if target_id.blank?
|
43
43
|
g = Resource.new(rdf_subject)
|
@@ -24,6 +24,10 @@ module Valkyrie::Persistence::Fedora
|
|
24
24
|
Valkyrie::Persistence::Fedora::Persister.new(adapter: self)
|
25
25
|
end
|
26
26
|
|
27
|
+
def id
|
28
|
+
@id ||= Valkyrie::ID.new(Digest::MD5.hexdigest(connection_prefix))
|
29
|
+
end
|
30
|
+
|
27
31
|
def resource_factory
|
28
32
|
Valkyrie::Persistence::Fedora::Persister::ResourceFactory.new(adapter: self)
|
29
33
|
end
|
@@ -63,6 +63,11 @@ module Valkyrie::Persistence::Fedora
|
|
63
63
|
uri_for(:valkyrie_time)
|
64
64
|
end
|
65
65
|
|
66
|
+
# @return [RDF::URI]
|
67
|
+
def self.optimistic_lock_token
|
68
|
+
uri_for(:optimistic_lock_token)
|
69
|
+
end
|
70
|
+
|
66
71
|
# Cast the property to a URI in the namespace
|
67
72
|
# @param property [Symbol]
|
68
73
|
# @return [RDF::URI]
|
@@ -22,6 +22,8 @@ module Valkyrie::Persistence::Fedora
|
|
22
22
|
graph_resource
|
23
23
|
end
|
24
24
|
|
25
|
+
# Filter resource properties to remove properties that should not be persisted to Fedora.
|
26
|
+
# * new_record is a virtual property for marking unsaved objects
|
25
27
|
def properties
|
26
28
|
resource_attributes.keys - [:new_record]
|
27
29
|
end
|
@@ -11,7 +11,7 @@ module Valkyrie::Persistence::Fedora
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def convert
|
14
|
-
Valkyrie::Types::Anything[attributes]
|
14
|
+
populate_native_lock(Valkyrie::Types::Anything[attributes])
|
15
15
|
end
|
16
16
|
|
17
17
|
def attributes
|
@@ -20,6 +20,17 @@ module Valkyrie::Persistence::Fedora
|
|
20
20
|
.merge(id: id, new_record: false)
|
21
21
|
end
|
22
22
|
|
23
|
+
# Get Fedora's lastModified value from the LDP response
|
24
|
+
def populate_native_lock(resource)
|
25
|
+
return resource unless resource.respond_to?(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK)
|
26
|
+
lastmod = object.response_graph.first_object([nil, RDF::URI("http://fedora.info/definitions/v4/repository#lastModified"), nil])
|
27
|
+
return resource unless lastmod
|
28
|
+
|
29
|
+
token = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: "native-#{adapter.id}", token: DateTime.parse(lastmod.to_s).httpdate)
|
30
|
+
resource.send(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK) << token
|
31
|
+
resource
|
32
|
+
end
|
33
|
+
|
23
34
|
def id
|
24
35
|
id_property.present? ? Valkyrie::ID.new(id_property) : adapter.uri_to_id(object.subject_uri)
|
25
36
|
end
|
@@ -237,6 +248,18 @@ module Valkyrie::Persistence::Fedora
|
|
237
248
|
end
|
238
249
|
end
|
239
250
|
|
251
|
+
class ValkyrieOptimisticLockToken < ::Valkyrie::ValueMapper
|
252
|
+
FedoraValue.register(self)
|
253
|
+
def self.handles?(value)
|
254
|
+
value.statement.object.is_a?(RDF::Literal) && value.statement.object.datatype == PermissiveSchema.optimistic_lock_token
|
255
|
+
end
|
256
|
+
|
257
|
+
def result
|
258
|
+
value.statement.object = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: value.adapter.id, token: value.statement.object.to_s)
|
259
|
+
calling_mapper.for(Property.new(statement: value.statement, scope: value.scope, adapter: value.adapter)).result
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
240
263
|
class InternalURI < ::Valkyrie::ValueMapper
|
241
264
|
FedoraValue.register(self)
|
242
265
|
def self.handles?(value)
|
@@ -295,10 +318,17 @@ module Valkyrie::Persistence::Fedora
|
|
295
318
|
@property = property
|
296
319
|
end
|
297
320
|
|
321
|
+
# Apply as a single value by default, if there are multiple then
|
322
|
+
# create an array. Done to support single values - if the resource is
|
323
|
+
# a Set or Array then it'll cast the single value back to an array
|
324
|
+
# appropriately.
|
298
325
|
def apply_to(hsh)
|
299
326
|
return if blacklist?(key)
|
300
|
-
hsh[key.to_sym]
|
301
|
-
|
327
|
+
hsh[key.to_sym] = if hsh.key?(key.to_sym)
|
328
|
+
Array.wrap(hsh[key.to_sym]) + cast_array(values)
|
329
|
+
else
|
330
|
+
values
|
331
|
+
end
|
302
332
|
end
|
303
333
|
|
304
334
|
def key
|
@@ -6,6 +6,8 @@ module Valkyrie::Persistence::Fedora
|
|
6
6
|
require 'valkyrie/persistence/fedora/persister/alternate_identifier'
|
7
7
|
attr_reader :adapter
|
8
8
|
delegate :connection, :base_path, :resource_factory, to: :adapter
|
9
|
+
|
10
|
+
# @note (see Valkyrie::Persistence::Memory::Persister#initialize)
|
9
11
|
def initialize(adapter:)
|
10
12
|
@adapter = adapter
|
11
13
|
end
|
@@ -13,21 +15,26 @@ module Valkyrie::Persistence::Fedora
|
|
13
15
|
# (see Valkyrie::Persistence::Memory::Persister#save)
|
14
16
|
def save(resource:)
|
15
17
|
initialize_repository
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
internal_resource = resource.dup
|
19
|
+
internal_resource.created_at ||= Time.current
|
20
|
+
internal_resource.updated_at ||= Time.current
|
21
|
+
validate_lock_token(internal_resource)
|
22
|
+
native_lock = native_lock_token(internal_resource)
|
23
|
+
generate_lock_token(internal_resource)
|
24
|
+
orm = resource_factory.from_resource(resource: internal_resource)
|
25
|
+
alternate_resources = find_or_create_alternate_ids(internal_resource)
|
26
|
+
|
27
|
+
if !orm.new? || internal_resource.id
|
28
|
+
cleanup_alternate_resources(internal_resource) if alternate_resources
|
29
|
+
orm.update { |req| update_request_headers(req, native_lock) }
|
25
30
|
else
|
26
31
|
orm.create
|
27
32
|
end
|
28
33
|
persisted_resource = resource_factory.to_resource(object: orm)
|
29
34
|
|
30
35
|
alternate_resources ? save_reference_to_resource(persisted_resource, alternate_resources) : persisted_resource
|
36
|
+
rescue Ldp::PreconditionFailed
|
37
|
+
raise Valkyrie::Persistence::StaleObjectError, internal_resource.id.to_s
|
31
38
|
end
|
32
39
|
|
33
40
|
# (see Valkyrie::Persistence::Memory::Persister#save_all)
|
@@ -35,6 +42,9 @@ module Valkyrie::Persistence::Fedora
|
|
35
42
|
resources.map do |resource|
|
36
43
|
save(resource: resource)
|
37
44
|
end
|
45
|
+
rescue Valkyrie::Persistence::StaleObjectError
|
46
|
+
# blank out the message / id
|
47
|
+
raise Valkyrie::Persistence::StaleObjectError
|
38
48
|
end
|
39
49
|
|
40
50
|
# (see Valkyrie::Persistence::Memory::Persister#delete)
|
@@ -70,13 +80,6 @@ module Valkyrie::Persistence::Fedora
|
|
70
80
|
|
71
81
|
private
|
72
82
|
|
73
|
-
def ensure_multiple_values!(resource)
|
74
|
-
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id, :references).select do |_k, v|
|
75
|
-
!v.nil? && !v.is_a?(Array)
|
76
|
-
end
|
77
|
-
raise ::Valkyrie::Persistence::UnsupportedDatatype, "#{resource}: #{bad_keys.keys} have non-array values, which can not be persisted by Valkyrie. Cast to arrays." unless bad_keys.keys.empty?
|
78
|
-
end
|
79
|
-
|
80
83
|
def find_or_create_alternate_ids(resource)
|
81
84
|
return nil unless resource.try(:alternate_ids)
|
82
85
|
|
@@ -107,5 +110,43 @@ module Valkyrie::Persistence::Fedora
|
|
107
110
|
|
108
111
|
resource
|
109
112
|
end
|
113
|
+
|
114
|
+
# @note Fedora's last modified response is not granular enough to produce an effective lock token
|
115
|
+
# therefore, we use the same implementation as the memory adapter. This could fail to lock a
|
116
|
+
# resource if Fedora updated this resource between the time it was saved and Valkyrie created
|
117
|
+
# the token.
|
118
|
+
def generate_lock_token(resource)
|
119
|
+
return unless resource.optimistic_locking_enabled?
|
120
|
+
token = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: adapter.id, token: Time.now.to_r)
|
121
|
+
resource.send("#{Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK}=", token)
|
122
|
+
end
|
123
|
+
|
124
|
+
def validate_lock_token(resource)
|
125
|
+
return unless resource.optimistic_locking_enabled?
|
126
|
+
return if resource.id.blank?
|
127
|
+
|
128
|
+
current_lock_token = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].find { |lock_token| lock_token.adapter_id == adapter.id }
|
129
|
+
return if current_lock_token.blank?
|
130
|
+
|
131
|
+
retrieved_lock_tokens = adapter.query_service.find_by(id: resource.id)[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK]
|
132
|
+
retrieved_lock_token = retrieved_lock_tokens.find { |lock_token| lock_token.adapter_id == adapter.id }
|
133
|
+
return if retrieved_lock_token.blank?
|
134
|
+
|
135
|
+
raise Valkyrie::Persistence::StaleObjectError, resource.id.to_s unless current_lock_token == retrieved_lock_token
|
136
|
+
end
|
137
|
+
|
138
|
+
# Retrieve the lock token that holds Fedora's system-managed last-modified date
|
139
|
+
def native_lock_token(resource)
|
140
|
+
return unless resource.optimistic_locking_enabled?
|
141
|
+
resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].find { |lock_token| lock_token.adapter_id == "native-#{adapter.id}" }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Set Fedora request headers:
|
145
|
+
# * `Prefer: handling=lenient; received="minimal"` allows us to avoid sending all server-managed triples
|
146
|
+
# * `If-Unmodified-Since` triggers Fedora's server-side optimistic locking
|
147
|
+
def update_request_headers(request, lock_token)
|
148
|
+
request.headers["Prefer"] = "handling=lenient; received=\"minimal\""
|
149
|
+
request.headers["If-Unmodified-Since"] = lock_token.token if lock_token
|
150
|
+
end
|
110
151
|
end
|
111
152
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module Valkyrie::Persistence
|
4
4
|
# Implements the DataMapper Pattern to store metadata into Fedora
|
5
5
|
module Fedora
|
6
|
+
require 'active_triples'
|
7
|
+
require 'active_fedora'
|
6
8
|
require 'valkyrie/persistence/fedora/permissive_schema'
|
7
9
|
require 'valkyrie/persistence/fedora/metadata_adapter'
|
8
10
|
require 'valkyrie/persistence/fedora/persister'
|
@@ -6,8 +6,10 @@ module Valkyrie::Persistence::Memory
|
|
6
6
|
class Persister
|
7
7
|
attr_reader :adapter
|
8
8
|
delegate :cache, to: :adapter
|
9
|
+
|
9
10
|
# @param adapter [Valkyrie::Persistence::Memory::MetadataAdapter] The memory adapter which
|
10
11
|
# holds the cache for this persister.
|
12
|
+
# @note Many persister methods are part of Valkyrie's public API, but instantiation itself is not
|
11
13
|
def initialize(adapter)
|
12
14
|
@adapter = adapter
|
13
15
|
end
|
@@ -16,13 +18,18 @@ module Valkyrie::Persistence::Memory
|
|
16
18
|
# @return [Valkyrie::Resource] The resource with an `#id` value generated by the
|
17
19
|
# persistence backend.
|
18
20
|
def save(resource:)
|
19
|
-
resource
|
20
|
-
|
21
|
-
resource
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
raise Valkyrie::Persistence::StaleObjectError, resource.id unless valid_lock?(resource)
|
22
|
+
|
23
|
+
# duplicate the resource so we are not creating side effects on the caller's resource
|
24
|
+
internal_resource = resource.dup
|
25
|
+
|
26
|
+
internal_resource = generate_id(internal_resource) if internal_resource.id.blank?
|
27
|
+
internal_resource.created_at ||= Time.current
|
28
|
+
internal_resource.updated_at = Time.current
|
29
|
+
internal_resource.new_record = false
|
30
|
+
generate_lock_token(internal_resource)
|
31
|
+
normalize_dates!(internal_resource)
|
32
|
+
cache[internal_resource.id] = internal_resource
|
26
33
|
end
|
27
34
|
|
28
35
|
# @param resources [Array<Valkyrie::Resource>] List of resources to save.
|
@@ -32,6 +39,9 @@ module Valkyrie::Persistence::Memory
|
|
32
39
|
resources.map do |resource|
|
33
40
|
save(resource: resource)
|
34
41
|
end
|
42
|
+
rescue Valkyrie::Persistence::StaleObjectError
|
43
|
+
# Re-raising with no error message to prevent confusion
|
44
|
+
raise Valkyrie::Persistence::StaleObjectError
|
35
45
|
end
|
36
46
|
|
37
47
|
# @param resource [Valkyrie::Resource] The resource to delete from the persistence
|
@@ -51,13 +61,6 @@ module Valkyrie::Persistence::Memory
|
|
51
61
|
resource.new(id: SecureRandom.uuid)
|
52
62
|
end
|
53
63
|
|
54
|
-
def ensure_multiple_values!(resource)
|
55
|
-
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id).select do |_k, v|
|
56
|
-
!v.nil? && !v.is_a?(Array)
|
57
|
-
end
|
58
|
-
raise ::Valkyrie::Persistence::UnsupportedDatatype, "#{resource}: #{bad_keys.keys} have non-array values, which can not be persisted by Valkyrie. Cast to arrays." unless bad_keys.keys.empty?
|
59
|
-
end
|
60
|
-
|
61
64
|
def normalize_dates!(resource)
|
62
65
|
resource.attributes.each { |k, v| resource.send("#{k}=", normalize_date_values(v)) }
|
63
66
|
end
|
@@ -72,5 +75,25 @@ module Valkyrie::Persistence::Memory
|
|
72
75
|
return value.to_datetime.utc if value.is_a?(Time)
|
73
76
|
value
|
74
77
|
end
|
78
|
+
|
79
|
+
def generate_lock_token(resource)
|
80
|
+
return unless resource.optimistic_locking_enabled?
|
81
|
+
token = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: adapter.id, token: Time.now.to_r)
|
82
|
+
resource.send("#{Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK}=", token)
|
83
|
+
end
|
84
|
+
|
85
|
+
def valid_lock?(resource)
|
86
|
+
return true unless resource.optimistic_locking_enabled?
|
87
|
+
|
88
|
+
cached_resource = cache[resource.id]
|
89
|
+
return true if cached_resource.blank?
|
90
|
+
|
91
|
+
resource_lock_tokens = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK]
|
92
|
+
resource_value = resource_lock_tokens.find { |lock_token| lock_token.adapter_id == adapter.id }
|
93
|
+
return true if resource_value.blank?
|
94
|
+
|
95
|
+
cached_value = cached_resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].first
|
96
|
+
cached_value == resource_value
|
97
|
+
end
|
75
98
|
end
|
76
99
|
end
|