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.
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