valkyrie 1.1.2 → 1.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +1 -1
  3. data/.rubocop_todo.yml +5 -0
  4. data/CHANGELOG.md +28 -0
  5. data/README.md +43 -7
  6. data/db/migrate/20180802220739_add_optimistic_locking_to_orm_resources.rb +6 -0
  7. data/lib/generators/valkyrie/templates/resource.rb.erb +0 -1
  8. data/lib/valkyrie/change_set.rb +4 -0
  9. data/lib/valkyrie/id.rb +5 -0
  10. data/lib/valkyrie/metadata_adapter.rb +1 -0
  11. data/lib/valkyrie/persistence/composite_persister.rb +12 -1
  12. data/lib/valkyrie/persistence/fedora/list_node.rb +3 -3
  13. data/lib/valkyrie/persistence/fedora/metadata_adapter.rb +4 -0
  14. data/lib/valkyrie/persistence/fedora/permissive_schema.rb +5 -0
  15. data/lib/valkyrie/persistence/fedora/persister/alternate_identifier.rb +0 -1
  16. data/lib/valkyrie/persistence/fedora/persister/model_converter.rb +2 -0
  17. data/lib/valkyrie/persistence/fedora/persister/orm_converter.rb +33 -3
  18. data/lib/valkyrie/persistence/fedora/persister.rb +57 -16
  19. data/lib/valkyrie/persistence/fedora/query_service.rb +2 -0
  20. data/lib/valkyrie/persistence/fedora.rb +2 -0
  21. data/lib/valkyrie/persistence/memory/metadata_adapter.rb +4 -0
  22. data/lib/valkyrie/persistence/memory/persister.rb +37 -14
  23. data/lib/valkyrie/persistence/memory/query_service.rb +2 -0
  24. data/lib/valkyrie/persistence/optimistic_lock_token.rb +34 -0
  25. data/lib/valkyrie/persistence/postgres/metadata_adapter.rb +11 -2
  26. data/lib/valkyrie/persistence/postgres/orm_converter.rb +35 -5
  27. data/lib/valkyrie/persistence/postgres/persister.rb +10 -12
  28. data/lib/valkyrie/persistence/postgres/query_service.rb +5 -4
  29. data/lib/valkyrie/persistence/postgres/resource_converter.rb +21 -1
  30. data/lib/valkyrie/persistence/postgres/resource_factory.rb +22 -18
  31. data/lib/valkyrie/persistence/solr/metadata_adapter.rb +5 -1
  32. data/lib/valkyrie/persistence/solr/model_converter.rb +32 -2
  33. data/lib/valkyrie/persistence/solr/orm_converter.rb +22 -5
  34. data/lib/valkyrie/persistence/solr/persister.rb +2 -0
  35. data/lib/valkyrie/persistence/solr/query_service.rb +2 -0
  36. data/lib/valkyrie/persistence/solr/repository.rb +21 -8
  37. data/lib/valkyrie/persistence/solr/resource_factory.rb +6 -3
  38. data/lib/valkyrie/persistence.rb +10 -3
  39. data/lib/valkyrie/resource/access_controls.rb +0 -1
  40. data/lib/valkyrie/resource.rb +31 -8
  41. data/lib/valkyrie/specs/shared_specs/change_set_persister.rb +0 -1
  42. data/lib/valkyrie/specs/shared_specs/metadata_adapter.rb +9 -0
  43. data/lib/valkyrie/specs/shared_specs/persister.rb +134 -7
  44. data/lib/valkyrie/specs/shared_specs/queries.rb +20 -2
  45. data/lib/valkyrie/specs/shared_specs/resource.rb +0 -1
  46. data/lib/valkyrie/specs/shared_specs/storage_adapter.rb +0 -1
  47. data/lib/valkyrie/storage/fedora.rb +29 -20
  48. data/lib/valkyrie/types.rb +11 -1
  49. data/lib/valkyrie/version.rb +1 -1
  50. data/lib/valkyrie.rb +0 -2
  51. metadata +7 -6
  52. 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(adapter: self)
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(attributes.merge(new_record: false))
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.map do |value|
121
- calling_mapper.for(value).result
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
- resources.map do |resource|
24
- save(resource: resource)
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 :adapter
11
- delegate :resource_factory, to: :adapter
10
+ attr_reader :resource_factory
12
11
  delegate :orm_class, to: :resource_factory
13
- def initialize(adapter:)
14
- @adapter = adapter
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.metadata.merge!(resource.attributes.except(:id, :internal_resource, :created_at, :updated_at))
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
- class << self
9
- # @param object [Valkyrie::Persistence::Postgres::ORM::Resource] AR
10
- # record to be converted.
11
- # @return [Valkyrie::Resource] Model representation of the AR record.
12
- def to_resource(object:)
13
- ::Valkyrie::Persistence::Postgres::ORMConverter.new(object).convert!
14
- end
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
- # @param resource [Valkyrie::Resource] Model to be converted to ActiveRecord.
17
- # @return [Valkyrie::Persistence::Postgres::ORM::Resource] ActiveRecord
18
- # resource for the Valkyrie resource.
19
- def from_resource(resource:)
20
- ::Valkyrie::Persistence::Postgres::ResourceConverter.new(resource, resource_factory: self).convert!
21
- end
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
- # Accessor for the ActiveRecord class which all Postgres resources are an
24
- # instance of.
25
- def orm_class
26
- ::Valkyrie::Persistence::Postgres::ORM::Resource
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, internal_resource: internal_resource, created_at: created_at, updated_at: updated_at)
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.map do |element|
126
- calling_mapper.for(element).result
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
- connection.add documents, params: COMMIT_PARAMS
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 ensure_multiple_values!(resource)
42
- bad_keys = resource.attributes.except(:internal_resource, :alternate_ids, :created_at, :updated_at, :new_record, :id).select do |_k, v|
43
- !v.nil? && !v.is_a?(Array)
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
- def initialize(resource_indexer:)
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.
@@ -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
@@ -6,7 +6,6 @@ module Valkyrie
6
6
  # @example
7
7
  # class CustomResource < Valkyrie::Resource
8
8
  # include Valkyrie::Resource::AccessControls
9
- # attribute :id, Valkyrie::Types::ID.optional
10
9
  # attribute :title
11
10
  # attribute :member_ids
12
11
  # attribute :nested_resource
@@ -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 :internal_resource, Valkyrie::Types::Any.default(subclass.to_s)
27
- subclass.attribute :created_at, Valkyrie::Types::DateTime.optional
28
- subclass.attribute :updated_at, Valkyrie::Types::DateTime.optional
29
- subclass.attribute :new_record, Types::Bool.default(true)
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
- def self.attribute(name, type = Valkyrie::Types::Set.optional)
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