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
@@ -6,8 +6,10 @@ module Valkyrie::Persistence::Memory
|
|
6
6
|
# @note Documentation for Query Services in general is maintained here.
|
7
7
|
attr_reader :adapter, :query_handlers
|
8
8
|
delegate :cache, to: :adapter
|
9
|
+
|
9
10
|
# @param adapter [Valkyrie::Persistence::Memory::MetadataAdapter] The adapter which
|
10
11
|
# has the cache to query.
|
12
|
+
# @note Many query service methods are part of Valkyrie's public API, but instantiation itself is not
|
11
13
|
def initialize(adapter:)
|
12
14
|
@adapter = adapter
|
13
15
|
@query_handlers = []
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Valkyrie::Persistence
|
3
|
+
class OptimisticLockToken
|
4
|
+
attr_reader :adapter_id, :token
|
5
|
+
|
6
|
+
# @param [Valkyrie::ID] adapter_id
|
7
|
+
# @param [String, Integer] token for the adapter
|
8
|
+
def initialize(adapter_id:, token:)
|
9
|
+
@adapter_id = adapter_id
|
10
|
+
@token = token
|
11
|
+
end
|
12
|
+
|
13
|
+
# Serializing lock tokens makes them easy to cast to strings and back.
|
14
|
+
# Primary use case is for embedding one in a form as a hidden field
|
15
|
+
def serialize
|
16
|
+
"lock_token:#{adapter_id}:#{token}"
|
17
|
+
end
|
18
|
+
alias to_s serialize
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
return false unless other.is_a?(self.class)
|
22
|
+
return false unless adapter_id == other.adapter_id
|
23
|
+
token == other.token
|
24
|
+
end
|
25
|
+
|
26
|
+
# Deserializing lock tokens means that we can then use the adapter id and the lock token value
|
27
|
+
def self.deserialize(serialized_token)
|
28
|
+
token_parts = serialized_token.to_s.split(":", 3)
|
29
|
+
adapter_id = token_parts[1]
|
30
|
+
token = token_parts[2]
|
31
|
+
new(adapter_id: Valkyrie::ID.new(adapter_id), token: token)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -16,12 +16,21 @@ module Valkyrie::Persistence::Postgres
|
|
16
16
|
|
17
17
|
# @return [Class] {Valkyrie::Persistence::Postgres::QueryService}
|
18
18
|
def query_service
|
19
|
-
@query_service ||= Valkyrie::Persistence::Postgres::QueryService.new(
|
19
|
+
@query_service ||= Valkyrie::Persistence::Postgres::QueryService.new(
|
20
|
+
resource_factory: resource_factory
|
21
|
+
)
|
20
22
|
end
|
21
23
|
|
22
24
|
# @return [Class] {Valkyrie::Persistence::Postgres::ResourceFactory}
|
23
25
|
def resource_factory
|
24
|
-
Valkyrie::Persistence::Postgres::ResourceFactory
|
26
|
+
@resource_factory ||= Valkyrie::Persistence::Postgres::ResourceFactory.new(adapter: self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def id
|
30
|
+
@id ||= begin
|
31
|
+
to_hash = "#{resource_factory.orm_class.connection_config['host']}:#{resource_factory.orm_class.connection_config['database']}"
|
32
|
+
Valkyrie::ID.new(Digest::MD5.hexdigest(to_hash))
|
33
|
+
end
|
25
34
|
end
|
26
35
|
end
|
27
36
|
end
|
@@ -3,9 +3,10 @@ module Valkyrie::Persistence::Postgres
|
|
3
3
|
# Responsible for converting a
|
4
4
|
# {Valkyrie::Persistence::Postgres::ORM::Resource} to a {Valkyrie::Resource}
|
5
5
|
class ORMConverter
|
6
|
-
attr_reader :orm_object
|
7
|
-
def initialize(orm_object)
|
6
|
+
attr_reader :orm_object, :resource_factory
|
7
|
+
def initialize(orm_object, resource_factory:)
|
8
8
|
@orm_object = orm_object
|
9
|
+
@resource_factory = resource_factory
|
9
10
|
end
|
10
11
|
|
11
12
|
# Create a new instance of the class described in attributes[:internal_resource]
|
@@ -17,7 +18,30 @@ module Valkyrie::Persistence::Postgres
|
|
17
18
|
private
|
18
19
|
|
19
20
|
def resource
|
20
|
-
resource_klass.new(
|
21
|
+
resource_klass.new(
|
22
|
+
attributes.merge(
|
23
|
+
new_record: false,
|
24
|
+
Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK => lock_token
|
25
|
+
)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def lock_token
|
30
|
+
return lock_token_warning unless orm_object.class.column_names.include?("lock_version")
|
31
|
+
@lock_token ||=
|
32
|
+
Valkyrie::Persistence::OptimisticLockToken.new(
|
33
|
+
adapter_id: resource_factory.adapter_id,
|
34
|
+
token: orm_object.lock_version
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def lock_token_warning
|
39
|
+
return nil unless resource_klass.optimistic_locking_enabled?
|
40
|
+
warn "[MIGRATION REQUIRED] You have loaded a resource from the Postgres adapter with " \
|
41
|
+
"optimistic locking enabled, but the necessary migrations have not been run. \n" \
|
42
|
+
"Please run `bin/rails valkyrie_engine:install:migrations && bin/rails db:migrate` " \
|
43
|
+
"to enable this feature for Postgres."
|
44
|
+
nil
|
21
45
|
end
|
22
46
|
|
23
47
|
def resource_klass
|
@@ -117,8 +141,14 @@ module Valkyrie::Persistence::Postgres
|
|
117
141
|
end
|
118
142
|
|
119
143
|
def result
|
120
|
-
value
|
121
|
-
|
144
|
+
# Cast single-valued arrays to the first value, let Types::Set and
|
145
|
+
# Types::Array handle converting it back.
|
146
|
+
if value.length == 1
|
147
|
+
calling_mapper.for(value.first).result
|
148
|
+
else
|
149
|
+
value.map do |value|
|
150
|
+
calling_mapper.for(value).result
|
151
|
+
end
|
122
152
|
end
|
123
153
|
end
|
124
154
|
end
|
@@ -6,23 +6,30 @@ module Valkyrie::Persistence::Postgres
|
|
6
6
|
class Persister
|
7
7
|
attr_reader :adapter
|
8
8
|
delegate :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
|
12
14
|
|
13
15
|
# (see Valkyrie::Persistence::Memory::Persister#save)
|
14
16
|
def save(resource:)
|
15
|
-
ensure_multiple_values!(resource)
|
16
17
|
orm_object = resource_factory.from_resource(resource: resource)
|
17
18
|
orm_object.save!
|
18
19
|
resource_factory.to_resource(object: orm_object)
|
20
|
+
rescue ActiveRecord::StaleObjectError
|
21
|
+
raise Valkyrie::Persistence::StaleObjectError, resource.id.to_s
|
19
22
|
end
|
20
23
|
|
21
24
|
# (see Valkyrie::Persistence::Memory::Persister#save_all)
|
22
25
|
def save_all(resources:)
|
23
|
-
|
24
|
-
|
26
|
+
resource_factory.orm_class.transaction do
|
27
|
+
resources.map do |resource|
|
28
|
+
save(resource: resource)
|
29
|
+
end
|
25
30
|
end
|
31
|
+
rescue Valkyrie::Persistence::StaleObjectError
|
32
|
+
raise Valkyrie::Persistence::StaleObjectError
|
26
33
|
end
|
27
34
|
|
28
35
|
# (see Valkyrie::Persistence::Memory::Persister#delete)
|
@@ -36,14 +43,5 @@ module Valkyrie::Persistence::Postgres
|
|
36
43
|
def wipe!
|
37
44
|
resource_factory.orm_class.delete_all
|
38
45
|
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def ensure_multiple_values!(resource)
|
43
|
-
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id).select do |_k, v|
|
44
|
-
!v.nil? && !v.is_a?(Array)
|
45
|
-
end
|
46
|
-
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?
|
47
|
-
end
|
48
46
|
end
|
49
47
|
end
|
@@ -7,11 +7,12 @@ module Valkyrie::Persistence::Postgres
|
|
7
7
|
#
|
8
8
|
# @see Valkyrie::Persistence::Postgres::MetadataAdapter
|
9
9
|
class QueryService
|
10
|
-
attr_reader :
|
11
|
-
delegate :resource_factory, to: :adapter
|
10
|
+
attr_reader :resource_factory
|
12
11
|
delegate :orm_class, to: :resource_factory
|
13
|
-
|
14
|
-
|
12
|
+
|
13
|
+
# @note (see Valkyrie::Persistence::Memory::QueryService#initialize)
|
14
|
+
def initialize(resource_factory:)
|
15
|
+
@resource_factory = resource_factory
|
15
16
|
end
|
16
17
|
|
17
18
|
# (see Valkyrie::Persistence::Memory::QueryService#find_all)
|
@@ -13,8 +13,28 @@ module Valkyrie::Persistence::Postgres
|
|
13
13
|
def convert!
|
14
14
|
orm_class.find_or_initialize_by(id: resource.id && resource.id.to_s).tap do |orm_object|
|
15
15
|
orm_object.internal_resource = resource.internal_resource
|
16
|
-
orm_object
|
16
|
+
process_lock_token(orm_object)
|
17
|
+
orm_object.metadata.merge!(attributes)
|
17
18
|
end
|
18
19
|
end
|
20
|
+
|
21
|
+
def process_lock_token(orm_object)
|
22
|
+
return unless resource.respond_to?(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK)
|
23
|
+
postgres_token = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].find do |token|
|
24
|
+
token.adapter_id == resource_factory.adapter_id
|
25
|
+
end
|
26
|
+
return unless postgres_token
|
27
|
+
orm_object.lock_version = postgres_token.token
|
28
|
+
end
|
29
|
+
|
30
|
+
# Convert attributes to all be arrays to better enable querying and
|
31
|
+
# "changing of minds" later on.
|
32
|
+
def attributes
|
33
|
+
Hash[
|
34
|
+
resource.attributes.except(:id, :internal_resource, :created_at, :updated_at).compact.map do |k, v|
|
35
|
+
[k, Array.wrap(v)]
|
36
|
+
end
|
37
|
+
]
|
38
|
+
end
|
19
39
|
end
|
20
40
|
end
|
@@ -5,26 +5,30 @@ module Valkyrie::Persistence::Postgres
|
|
5
5
|
# Provides access to generic methods for converting to/from
|
6
6
|
# {Valkyrie::Resource} and {Valkyrie::Persistence::Postgres::ORM::Resource}.
|
7
7
|
class ResourceFactory
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
attr_reader :adapter
|
9
|
+
delegate :id, to: :adapter, prefix: true
|
10
|
+
def initialize(adapter:)
|
11
|
+
@adapter = adapter
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param object [Valkyrie::Persistence::Postgres::ORM::Resource] AR
|
15
|
+
# record to be converted.
|
16
|
+
# @return [Valkyrie::Resource] Model representation of the AR record.
|
17
|
+
def to_resource(object:)
|
18
|
+
::Valkyrie::Persistence::Postgres::ORMConverter.new(object, resource_factory: self).convert!
|
19
|
+
end
|
15
20
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
21
|
+
# @param resource [Valkyrie::Resource] Model to be converted to ActiveRecord.
|
22
|
+
# @return [Valkyrie::Persistence::Postgres::ORM::Resource] ActiveRecord
|
23
|
+
# resource for the Valkyrie resource.
|
24
|
+
def from_resource(resource:)
|
25
|
+
::Valkyrie::Persistence::Postgres::ResourceConverter.new(resource, resource_factory: self).convert!
|
26
|
+
end
|
22
27
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
end
|
28
|
+
# Accessor for the ActiveRecord class which all Postgres resources are an
|
29
|
+
# instance of.
|
30
|
+
def orm_class
|
31
|
+
::Valkyrie::Persistence::Postgres::ORM::Resource
|
28
32
|
end
|
29
33
|
end
|
30
34
|
end
|
@@ -50,10 +50,14 @@ module Valkyrie::Persistence::Solr
|
|
50
50
|
)
|
51
51
|
end
|
52
52
|
|
53
|
+
def id
|
54
|
+
@id ||= Valkyrie::ID.new(Digest::MD5.hexdigest(connection.base_uri.to_s))
|
55
|
+
end
|
56
|
+
|
53
57
|
# @return [Valkyrie::Persistence::Solr::ResourceFactory] A resource factory
|
54
58
|
# to convert a resource to a solr document and back.
|
55
59
|
def resource_factory
|
56
|
-
Valkyrie::Persistence::Solr::ResourceFactory.new(resource_indexer: resource_indexer)
|
60
|
+
Valkyrie::Persistence::Solr::ResourceFactory.new(resource_indexer: resource_indexer, adapter: self)
|
57
61
|
end
|
58
62
|
|
59
63
|
class NullIndexer
|
@@ -40,13 +40,43 @@ module Valkyrie::Persistence::Solr
|
|
40
40
|
"id": id,
|
41
41
|
"join_id_ssi": "id-#{id}",
|
42
42
|
"created_at_dtsi": created_at
|
43
|
-
}.merge(attribute_hash)
|
43
|
+
}.merge(add_single_values(attribute_hash)).merge(lock_hash)
|
44
44
|
end
|
45
45
|
|
46
46
|
private
|
47
47
|
|
48
|
+
def add_single_values(attribute_hash)
|
49
|
+
attribute_hash.select do |k, v|
|
50
|
+
field = k.to_s.split("_").last
|
51
|
+
property = k.to_s.gsub("_#{field}", "")
|
52
|
+
next true if multivalued?(field)
|
53
|
+
next false if property == "internal_resource"
|
54
|
+
next false if v.length > 1
|
55
|
+
true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def multivalued?(field)
|
60
|
+
field.end_with?('m', 'mv')
|
61
|
+
end
|
62
|
+
|
63
|
+
def lock_hash
|
64
|
+
return {} unless resource.optimistic_locking_enabled? && lock_token.present?
|
65
|
+
{ _version_: lock_token }
|
66
|
+
end
|
67
|
+
|
68
|
+
def lock_token
|
69
|
+
@lock_token ||= begin
|
70
|
+
found_token = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK]
|
71
|
+
.find { |token| token.adapter_id == resource_factory.adapter_id }
|
72
|
+
return if found_token.nil?
|
73
|
+
found_token.token
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
48
77
|
def attribute_hash
|
49
78
|
properties.each_with_object({}) do |property, hsh|
|
79
|
+
next if property == Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK
|
50
80
|
attr = resource_attributes[property]
|
51
81
|
mapper_val = SolrMapperValue.for(Property.new(property, attr)).result
|
52
82
|
unless mapper_val.respond_to?(:apply_to)
|
@@ -272,7 +302,7 @@ module Valkyrie::Persistence::Solr
|
|
272
302
|
if value.value.length > 1000
|
273
303
|
[:tsim]
|
274
304
|
else
|
275
|
-
[:tsim, :ssim, :tesim]
|
305
|
+
[:tsim, :ssim, :tesim, :tsi, :ssi, :tesi]
|
276
306
|
end
|
277
307
|
end
|
278
308
|
end
|
@@ -2,9 +2,10 @@
|
|
2
2
|
module Valkyrie::Persistence::Solr
|
3
3
|
# Responsible for converting hashes from Solr into a {Valkyrie::Resource}
|
4
4
|
class ORMConverter
|
5
|
-
attr_reader :solr_document
|
6
|
-
def initialize(solr_document)
|
5
|
+
attr_reader :solr_document, :resource_factory
|
6
|
+
def initialize(solr_document, resource_factory:)
|
7
7
|
@solr_document = solr_document
|
8
|
+
@resource_factory = resource_factory
|
8
9
|
end
|
9
10
|
|
10
11
|
def convert!
|
@@ -24,7 +25,11 @@ module Valkyrie::Persistence::Solr
|
|
24
25
|
end
|
25
26
|
|
26
27
|
def attributes
|
27
|
-
attribute_hash.merge("id" => id,
|
28
|
+
attribute_hash.merge("id" => id,
|
29
|
+
internal_resource: internal_resource,
|
30
|
+
created_at: created_at,
|
31
|
+
updated_at: updated_at,
|
32
|
+
Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK => token)
|
28
33
|
end
|
29
34
|
|
30
35
|
def created_at
|
@@ -35,6 +40,14 @@ module Valkyrie::Persistence::Solr
|
|
35
40
|
DateTime.parse(solr_document["timestamp"] || solr_document.fetch("created_at_dtsi").to_s).utc
|
36
41
|
end
|
37
42
|
|
43
|
+
def token
|
44
|
+
Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: resource_factory.adapter_id, token: version)
|
45
|
+
end
|
46
|
+
|
47
|
+
def version
|
48
|
+
solr_document.fetch('_version_', nil)
|
49
|
+
end
|
50
|
+
|
38
51
|
def id
|
39
52
|
solr_document.fetch('id').sub(/^id-/, '')
|
40
53
|
end
|
@@ -122,8 +135,12 @@ module Valkyrie::Persistence::Solr
|
|
122
135
|
end
|
123
136
|
|
124
137
|
def result
|
125
|
-
value.
|
126
|
-
calling_mapper.for(
|
138
|
+
if value.length == 1
|
139
|
+
calling_mapper.for(value.first).result
|
140
|
+
else
|
141
|
+
value.map do |element|
|
142
|
+
calling_mapper.for(element).result
|
143
|
+
end
|
127
144
|
end
|
128
145
|
end
|
129
146
|
end
|
@@ -7,8 +7,10 @@ module Valkyrie::Persistence::Solr
|
|
7
7
|
class Persister
|
8
8
|
attr_reader :adapter
|
9
9
|
delegate :connection, :resource_factory, to: :adapter
|
10
|
+
|
10
11
|
# @param adapter [Valkyrie::Persistence::Solr::MetadataAdapter] The adapter with the
|
11
12
|
# configured solr connection.
|
13
|
+
# @note (see Valkyrie::Persistence::Memory::Persister#initialize)
|
12
14
|
def initialize(adapter:)
|
13
15
|
@adapter = adapter
|
14
16
|
end
|
@@ -4,8 +4,10 @@ module Valkyrie::Persistence::Solr
|
|
4
4
|
# Query Service for Solr MetadataAdapter.
|
5
5
|
class QueryService
|
6
6
|
attr_reader :connection, :resource_factory
|
7
|
+
|
7
8
|
# @param connection [RSolr::Client]
|
8
9
|
# @param resource_factory [Valkyrie::Persistence::Solr::ResourceFactory]
|
10
|
+
# @note (see Valkyrie::Persistence::Memory::QueryService#initialize)
|
9
11
|
def initialize(connection:, resource_factory:)
|
10
12
|
@connection = connection
|
11
13
|
@resource_factory = resource_factory
|
@@ -3,7 +3,7 @@ module Valkyrie::Persistence::Solr
|
|
3
3
|
# Responsible for handling the logic for persisting or deleting multiple
|
4
4
|
# objects into or out of solr.
|
5
5
|
class Repository
|
6
|
-
COMMIT_PARAMS = { softCommit: true }.freeze
|
6
|
+
COMMIT_PARAMS = { softCommit: true, versions: true }.freeze
|
7
7
|
|
8
8
|
attr_reader :resources, :connection, :resource_factory
|
9
9
|
def initialize(resources:, connection:, resource_factory:)
|
@@ -15,15 +15,30 @@ module Valkyrie::Persistence::Solr
|
|
15
15
|
def persist
|
16
16
|
documents = resources.map do |resource|
|
17
17
|
generate_id(resource) if resource.id.blank?
|
18
|
-
ensure_multiple_values!(resource)
|
19
18
|
solr_document(resource)
|
20
19
|
end
|
21
|
-
|
20
|
+
results = add_documents(documents)
|
21
|
+
versions = results["adds"]&.each_slice(2)&.to_h
|
22
22
|
documents.map do |document|
|
23
|
+
document["_version_"] = versions.fetch(document[:id])
|
23
24
|
resource_factory.to_resource(object: document.stringify_keys)
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
28
|
+
# @param [Array<Hash>] array of Solr documents
|
29
|
+
# @return [RSolr::HashWithResponse]
|
30
|
+
# rubocop:disable Style/IfUnlessModifier
|
31
|
+
def add_documents(documents)
|
32
|
+
connection.add documents, params: COMMIT_PARAMS
|
33
|
+
rescue RSolr::Error::Http => exception
|
34
|
+
# Error 409 conflict is returned when versions do not match
|
35
|
+
if exception.response[:status] == 409
|
36
|
+
handle_409
|
37
|
+
end
|
38
|
+
raise exception
|
39
|
+
end
|
40
|
+
# rubocop:enable Style/IfUnlessModifier
|
41
|
+
|
27
42
|
def delete
|
28
43
|
connection.delete_by_id resources.map { |resource| resource.id.to_s }, params: COMMIT_PARAMS
|
29
44
|
resources
|
@@ -38,11 +53,9 @@ module Valkyrie::Persistence::Solr
|
|
38
53
|
resource.id = SecureRandom.uuid
|
39
54
|
end
|
40
55
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
end
|
45
|
-
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?
|
56
|
+
def handle_409
|
57
|
+
raise Valkyrie::Persistence::StaleObjectError if resources.count > 1
|
58
|
+
raise Valkyrie::Persistence::StaleObjectError, resources.first.id.to_s
|
46
59
|
end
|
47
60
|
end
|
48
61
|
end
|
@@ -5,16 +5,19 @@ module Valkyrie::Persistence::Solr
|
|
5
5
|
class ResourceFactory
|
6
6
|
require 'valkyrie/persistence/solr/orm_converter'
|
7
7
|
require 'valkyrie/persistence/solr/model_converter'
|
8
|
-
attr_reader :resource_indexer
|
9
|
-
|
8
|
+
attr_reader :resource_indexer, :adapter
|
9
|
+
delegate :id, to: :adapter, prefix: true
|
10
|
+
|
11
|
+
def initialize(resource_indexer:, adapter:)
|
10
12
|
@resource_indexer = resource_indexer
|
13
|
+
@adapter = adapter
|
11
14
|
end
|
12
15
|
|
13
16
|
# @param object [Hash] The solr document in a hash to convert to a
|
14
17
|
# resource.
|
15
18
|
# @return [Valkyrie::Resource]
|
16
19
|
def to_resource(object:)
|
17
|
-
ORMConverter.new(object).convert!
|
20
|
+
ORMConverter.new(object, resource_factory: self).convert!
|
18
21
|
end
|
19
22
|
|
20
23
|
# @param resource [Valkyrie::Resource] The resource to convert to a solr hash.
|
data/lib/valkyrie/persistence.rb
CHANGED
@@ -25,17 +25,24 @@ module Valkyrie
|
|
25
25
|
# @see lib/valkyrie/specs/shared_specs/persister.rb
|
26
26
|
#
|
27
27
|
module Persistence
|
28
|
+
require 'valkyrie/persistence/optimistic_lock_token'
|
28
29
|
require 'valkyrie/persistence/custom_query_container'
|
29
30
|
require 'valkyrie/persistence/memory'
|
30
|
-
require 'valkyrie/persistence/postgres'
|
31
|
-
require 'valkyrie/persistence/solr'
|
32
|
-
require 'valkyrie/persistence/fedora'
|
33
31
|
require 'valkyrie/persistence/composite_persister'
|
34
32
|
require 'valkyrie/persistence/delete_tracking_buffer'
|
35
33
|
require 'valkyrie/persistence/buffered_persister'
|
34
|
+
autoload :Postgres, 'valkyrie/persistence/postgres'
|
35
|
+
autoload :Solr, 'valkyrie/persistence/solr'
|
36
|
+
autoload :Fedora, 'valkyrie/persistence/fedora'
|
36
37
|
class ObjectNotFoundError < StandardError
|
37
38
|
end
|
38
39
|
class UnsupportedDatatype < StandardError
|
39
40
|
end
|
41
|
+
class StaleObjectError < StandardError
|
42
|
+
end
|
43
|
+
|
44
|
+
module Attributes
|
45
|
+
OPTIMISTIC_LOCK = :optimistic_lock_token
|
46
|
+
end
|
40
47
|
end
|
41
48
|
end
|
data/lib/valkyrie/resource.rb
CHANGED
@@ -4,7 +4,6 @@ module Valkyrie
|
|
4
4
|
# The base resource class for all Valkyrie metadata objects.
|
5
5
|
# @example Define a resource
|
6
6
|
# class Book < Valkyrie::Resource
|
7
|
-
# attribute :id, Valkyrie::Types::ID.optional
|
8
7
|
# attribute :member_ids, Valkyrie::Types::Array
|
9
8
|
# attribute :author
|
10
9
|
# end
|
@@ -23,10 +22,11 @@ module Valkyrie
|
|
23
22
|
def self.inherited(subclass)
|
24
23
|
super(subclass)
|
25
24
|
subclass.constructor_type :schema
|
26
|
-
subclass.attribute :
|
27
|
-
subclass.attribute :
|
28
|
-
subclass.attribute :
|
29
|
-
subclass.attribute :
|
25
|
+
subclass.attribute :id, Valkyrie::Types::ID.optional, internal: true
|
26
|
+
subclass.attribute :internal_resource, Valkyrie::Types::Any.default(subclass.to_s), internal: true
|
27
|
+
subclass.attribute :created_at, Valkyrie::Types::DateTime.optional, internal: true
|
28
|
+
subclass.attribute :updated_at, Valkyrie::Types::DateTime.optional, internal: true
|
29
|
+
subclass.attribute :new_record, Types::Bool.default(true), internal: true
|
30
30
|
end
|
31
31
|
|
32
32
|
# @return [Array<Symbol>] Array of fields defined for this class.
|
@@ -34,16 +34,27 @@ module Valkyrie
|
|
34
34
|
schema.keys.without(:new_record)
|
35
35
|
end
|
36
36
|
|
37
|
-
# Define an attribute.
|
37
|
+
# Define an attribute. Attributes are used to describe resources.
|
38
38
|
# @param name [Symbol]
|
39
39
|
# @param type [Dry::Types::Type]
|
40
40
|
# @note Overridden from {Dry::Struct} to make the default type
|
41
41
|
# {Valkyrie::Types::Set}
|
42
|
-
|
42
|
+
# @todo Remove ability to override built in attributes.
|
43
|
+
def self.attribute(name, type = Valkyrie::Types::Set.optional, internal: false)
|
44
|
+
if reserved_attributes.include?(name.to_sym) && schema[name] && !internal
|
45
|
+
warn "#{name} is a reserved attribute in Valkyrie::Resource and defined by it. You can remove your definition of `attribute :#{name}`. " \
|
46
|
+
"For now your version will be used, but in the next major version the type will be overridden. " \
|
47
|
+
"Called from #{Gem.location_of_caller.join(':')}"
|
48
|
+
schema.delete(name)
|
49
|
+
end
|
43
50
|
define_method("#{name}=") do |value|
|
44
51
|
instance_variable_set("@#{name}", self.class.schema[name].call(value))
|
45
52
|
end
|
46
|
-
super
|
53
|
+
super(name, type)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.reserved_attributes
|
57
|
+
[:id, :internal_resource, :created_at, :updated_at, :new_record]
|
47
58
|
end
|
48
59
|
|
49
60
|
# @return [ActiveModel::Name]
|
@@ -62,6 +73,18 @@ module Valkyrie
|
|
62
73
|
@_human_readable_type = val
|
63
74
|
end
|
64
75
|
|
76
|
+
def self.enable_optimistic_locking
|
77
|
+
attribute(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK, Valkyrie::Types::Set.of(Valkyrie::Types::OptimisticLockToken))
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.optimistic_locking_enabled?
|
81
|
+
schema.key?(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK)
|
82
|
+
end
|
83
|
+
|
84
|
+
def optimistic_locking_enabled?
|
85
|
+
self.class.optimistic_locking_enabled?
|
86
|
+
end
|
87
|
+
|
65
88
|
# @return [Hash] Hash of attributes
|
66
89
|
def attributes
|
67
90
|
to_h
|
@@ -4,7 +4,6 @@ RSpec.shared_examples 'a Valkyrie::ChangeSetPersister' do |*_flags|
|
|
4
4
|
raise 'adapter must be set with `let(:change_set_persister)`' unless defined? change_set_persister
|
5
5
|
class CustomResource < Valkyrie::Resource
|
6
6
|
include Valkyrie::Resource::AccessControls
|
7
|
-
attribute :id, Valkyrie::Types::ID.optional
|
8
7
|
attribute :title
|
9
8
|
attribute :member_ids
|
10
9
|
attribute :nested_resource
|
@@ -7,7 +7,16 @@ RSpec.shared_examples 'a Valkyrie::MetadataAdapter' do |passed_adapter|
|
|
7
7
|
subject { passed_adapter || adapter }
|
8
8
|
it { is_expected.to respond_to(:persister).with(0).arguments }
|
9
9
|
it { is_expected.to respond_to(:query_service).with(0).arguments }
|
10
|
+
it { is_expected.to respond_to(:id).with(0).arguments }
|
10
11
|
it "caches query_service so it can register custom queries" do
|
11
12
|
expect(subject.query_service.custom_queries.query_handlers.object_id).to eq subject.query_service.custom_queries.query_handlers.object_id
|
12
13
|
end
|
14
|
+
|
15
|
+
describe "#id" do
|
16
|
+
it "is a valid string representation of an MD5 hash" do
|
17
|
+
expect(adapter.id).to be_a Valkyrie::ID
|
18
|
+
expect(adapter.id.to_s.length).to eq 32
|
19
|
+
expect(adapter.id.to_s).to match(/^[a-f,0-9]+$/)
|
20
|
+
end
|
21
|
+
end
|
13
22
|
end
|